deeb8b67000e4faa4939f736a3a21004b47e249a
1537
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
deeb8b6700 |
feat(pf-encode): build against FFmpeg 9
apple / swift (pull_request) Successful in 1m53s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 2m34s
ci / web (pull_request) Successful in 2m32s
ci / docs-site (pull_request) Successful in 1m25s
ci / bun-nix (pull_request) Successful in 26s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m23s
android / android (pull_request) Successful in 6m47s
ci / rust-arm64 (pull_request) Successful in 8m49s
nix / flake (pull_request) Failing after 16m7s
ci / rust (pull_request) Successful in 23m39s
ffmpeg-next 8.1.0 could not accept FFmpeg 9 at all: ffmpeg-sys-next's version probe
covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 fell
outside what it knew how to bind. 9.0.0 widens that to 56..63, which is what actually
unblocks Arch. Bump both pins — the unconditional Linux dep and the optional Windows
amf-qsv one — and the lock with them.
No API drift to fix. The crate major is a CEILING, not a target: one source tree still
spans FFmpeg 7.x/libavcodec 61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper
symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 -> 9.0.0
unchanged. The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler
checks, so they were re-read against the real headers rather than trusted:
AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9, and
AVD3D11VADeviceContext gained two trailing UINTs in 8 that 7.1 lacks — which is why that
mirror deliberately stops at the common prefix, and why its assertions now say what they
do and do not buy you. They pin our layout, not libav's; a green build is not evidence.
The CI image is the step that makes this reach users. arch.yml deliberately runs no -Syu
("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg
8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the
soname dep the PKGBUILD now derives. Re-keying ci/ rebuilds it against ffmpeg 9.
Ubuntu and Windows deliberately stay put: the noble .deb bundles its own FFmpeg 8 behind
an rpath and strips the libav sonames from its Depends, and Windows bundles BtbN DLLs into
the signed installer — neither is exposed to the break, BtbN publishes no FFmpeg 9 build,
and moving either would re-qualify an encode stack to buy nothing.
Verified end to end on 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti): host
builds clean and links libavcodec.so.63/libavutil.so.61/libavfilter.so.12/libswscale.so.10
with no unresolved sonames; the ffmpeg-8 compat shim is gone and the service runs with
NRestarts=0 and answers 401 on :47990; pf-encode's 67 tests pass; and a live synthetic
encode drives real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC
stream (180/180 frames, FEC loopback 0 mismatches) with libavcodec.so.63 and
libnvidia-encode both mapped into the encoding process.
|
||
|
|
7f82bca9c0 |
fix(pf-dxvadec): a wrapped sentence turned "6." into an ordered list
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m10s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
ci / docs-site (pull_request) Successful in 2m3s
ci / rust-arm64 (pull_request) Successful in 2m16s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 4m59s
ci / rust (pull_request) Successful in 6m3s
A doc paragraph in `pic_av1.rs` wrapped so that "first at frame / 6. Releasing…" put `6.` at the start of a line. rustdoc reads that as an ordered-list item starting at 6, which makes the following unindented `///` line a lazy continuation — `clippy::doc_lazy_continuation`, denied by `-D warnings`. Reflowed so the number cannot begin a line. Prose is byte-identical in content; only the wrap points move. No code, no behaviour. WHY THIS MATTERS FOR THE TAG. `pf-dxvadec` is Windows-only, and no Windows leg runs on a push to main — so main being green proves nothing about this. The failure surfaces for the first time in a release tag's fan-out, which is exactly what happened to the FIRST v0.23.0 tag: it went red on Windows clippy for this same lint, and the cure was a tag re-point. Caught pre-tag by re-running the lazy-continuation scanner over the tree while preparing v0.25.0 (0 hits before this commit's parent merged the new decode crates, 1 after). Cannot be verified by compiling here — the crate does not build on macOS — so the evidence is the scanner plus the lint's own rule, not a clippy run. |
||
|
|
3d20f2c0e5 |
Merge pull request 'Three decode rungs were decoding into a surface they were predicting from' (#102) from integration/decode-aliasing-program into main
ci / bun-nix (push) Successful in 30s
ci / web (push) Successful in 1m22s
ci / docs-site (push) Successful in 1m24s
apple / swift (push) Successful in 1m35s
ci / rust-arm64 (push) Successful in 1m56s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 16s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 16s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 3m6s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m11s
android / android (push) Successful in 5m57s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m37s
apple / screenshots (push) Successful in 5m52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m24s
deb / build-publish-host (push) Successful in 6m20s
ci / rust (push) Canceled after 6m56s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
windows-host / package (push) Failing after 1m37s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 5m39s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m47s
arch / build-publish (push) Successful in 11m19s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 6m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 10m20s
flatpak / build-publish (push) Successful in 8m54s
Reviewed-on: #102 |
||
|
|
2b167595aa |
docs(client): the VAAPI rung has parity now — say what is actually left
ci / bun-nix (pull_request) Successful in 28s
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 30s
apple / swift (pull_request) Successful in 1m36s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m19s
ci / web (pull_request) Successful in 1m30s
ci / rust-arm64 (pull_request) Successful in 2m39s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m26s
android / android (pull_request) Successful in 4m31s
ci / rust (pull_request) Successful in 10m52s
Its rows still read "never frame-hash parity-checked: the rung exports a tiled dmabuf with no CPU-readable image, so parity needs a readback path that does not exist yet". That readback now exists, and all SEVEN legs came back bit-identical to libavcodec on RDNA3: vendored H.264 250/250, our host's low-delay H.264 120/120, vendored H.265 250/250, host low-delay H.265 120/120, HEVC Main 10 50/50 as P010, vendored AV1 250/250 of 274 decoded, and host low-delay 4K two-tile AV1 60/60. The two arms collapse into one, because the thing that split them — AV1 having evidence the other legs lacked — is gone. Every leg now has the same evidence. It stays `verified = false`, and the note says why in the words the unproven-rung test requires: it has NEVER run on a second vendor and has never been soaked. That is a real limit rather than a formality — every other verified pair in this table earned it on more than one part, and the D3D11VA AV1 row two entries up is a rung that passed on one vendor's driver while failing on another's. The second reason is not about evidence at all, and it belongs in the record rather than in a commit nobody reads later: flipping this flag is a ROUTING change. `native_rung_admitted` is `verified || !below.verified`, so a verified VAAPI outranks Vulkan Video on every Linux AMD and Intel client — the Steam Deck included. The parity result justifies that change; it should still be made on purpose, by someone who wants it, rather than arriving as a side effect of writing down a test result. |
||
|
|
a9e7c033c3 |
test(client/vaapi): the last rung of the ladder, finally checked in pixels — 7 legs, all bit-identical
Every other decode rung earns `verified` with frame-hash parity against
libavcodec. VAAPI could not: it hands out a DRM-PRIME dmabuf whose memory the
driver tiles, so nothing could read its decoded pixels back, and all four of its
legs sat at "never frame-hash parity-checked".
That was never bookkeeping. The D3D11VA AV1 rung decoded 250 frames, streamed
4K60 through a clean five-minute soak, and produced WRONG PIXELS for 186 of 250
frames on NVIDIA and 245 of 250 on Intel. It looked perfect on glass; only the
goldens caught it, and the same defect turned out to be in H.264 on two other
rungs. VAAPI was the one rung where that class of bug could still be sitting
with nothing able to see it.
It is not. Measured on .25 (Radeon 780M, RDNA3, radeonsi, Mesa 26.0.3, VA-API
1.23) on 2026-08-08, against the SAME golden files the Vulkan and D3D11VA rungs
are held to, read across the crate boundary rather than copied:
H.264 vendored vector 250/250 bit-identical (7 from the flush)
H.264 our host, low-delay 640x480 120/120 bit-identical (3 from the flush)
H.265 vendored vector 250/250 bit-identical (2 from the flush)
H.265 our host, low-delay 640x480 120/120 bit-identical (0 from the flush)
HEVC Main 10, P010 50/50 bit-identical (2 from the flush)
AV1 vendored vector 250/250 delivered of 274 decoded, and
display frame 0 byte-identical to
libavcodec's own PIXELS
AV1 our host, 4K two-tile 60/60 bit-identical
⚠ ONE vendor. AMD/radeonsi only; no Intel iHD box has run these legs.
The readback that made it possible:
* `pf-vaadec`'s `va` module gains `VAImage` and `VAImageFormat`, hand-declared
with every size and offset measured off libva 2.23.0's real headers by
`layout-probe.c` and pinned as compile-time assertions — the same discipline
the decode buffers already keep. The trap: `VAImage::width`/`height` are
16-bit, so `data_size` sits at 60 and not at the 64 counting 32-bit fields
gives, and every field after them is two bytes earlier than it looks.
* `pack_two_plane` is the pure geometry — the crop to the picture, the padding
columns dropped per row, and the chroma plane taken from the driver's OWN
`offsets[1]` rather than from `pitch * display_height`, which is the 1088-row
smear this program has already paid for once. It needs no device, so ten CPU
tests cover it on macOS and in the container.
* `video_vaapi_native::parity` drives the seven streams above through the
production entry point and hashes what the rung DELIVERS, in delivery order,
tail included — so the delivery path is under test as well as the decode, and
a frame's surface comes from its own release token rather than from an
inference about which pool entry holds which picture.
THE READBACK CANNOT REACH THE PRODUCTION PATH, and that is structural rather
than a promise. `vaDeriveImage`, `vaCreateImage`, `vaGetImage`, `vaMapBuffer`
and the rest are resolved by a `#[cfg(test)]` type that dlopens libva itself;
the production `Libva` gains no field; `sha2` is a dev dependency. A CPU test
scans this file's own source and fails if any of those symbols is dlsym'd
outside the harness, so a refactor cannot quietly undo it.
Derive is not guaranteed, so both routes are implemented and neither is
optional: `vaDeriveImage` first, `vaCreateImage` + `vaGetImage` as the fallback
(which also detiles), and if neither yields the pool's own fourcc the leg FAILS
naming what the driver gave it. There is no skip path — a parity test that
passes because it could not read anything is the failure mode this program has
been bitten by three times. Both answer on radeonsi, the first frame of every
leg is read through BOTH and they must agree, and `PF_VAAPI_READBACK=getimage`
reproduces the H.264 leg's 250/250 through the copying route alone, so the
fallback is exercised rather than merely written.
And it can fail — proven, not asserted. Planting the real geometry defect this
driver's layout makes visible (rows read contiguously, ignoring the 512-byte
pitch behind a 320-wide picture) fails at display frame 0 with the full
localisation: 68312 luma and 14998 chroma samples differing, max |delta| 255,
luma bounding box (0,1)..(319,239) — and with the goldens forced through one
route, 250/250 diverging with "suspect the readback geometry". `compare` and
`localise` also have CPU counterfactuals, and a hardware leg proves the readback
reads real and DISTINCT pixels and localises a one-byte flip to the exact pixel.
⚠ One thing the hardware legs do NOT cover, found by planting the other defect
and watching it do nothing: radeonsi's decode surfaces for every fixture here
have no VERTICAL padding — `offsets[1]` is exactly `pitch * height` — so the
chroma-plane trap is untested on this driver, and `pf-vaadec`'s
`reading_chroma_at_the_display_height_would_have_been_caught` is the only place
it is checked at all. `probe_this_machines_readback_routes` now prints the
derived layout and says which of the two it is, so the next driver answers for
itself instead of being assumed.
|
||
|
|
bfed711921 |
Merge remote-tracking branch 'origin/main' into audio/latency-overhaul
ci / bun-nix (pull_request) Successful in 33s
ci / web (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Successful in 1m21s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m13s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m36s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m9s
android / android (pull_request) Successful in 7m53s
ci / rust (pull_request) Successful in 12m23s
|
||
|
|
f926bab9f7 |
fix(client): the native VAAPI rung stopped dropping decoded frames on the floor
`finish` showed `outputs.last()` and retired every other picture an access unit bumped out of the DPB without ever displaying it, and nothing flushed the DPB at end of stream. Measured on .25 against the vendored vectors: 225 of 250 frames for H.264, 204 of 250 for H.265, 45 of 50 for HEVC Main 10. D3D11VA and Vulkan deliver every frame, so this was the rung's alone. All four legs now deliver 250 / 250 / 50 / 250. The same function carried a second defect. `DmabufFrame::keyframe` was stamped with the CURRENT access unit's `is_idr`, not the flag of the picture it was about to display, and on a reordering stream those are different pictures: the IDR is bumped out several units after it decodes and arrived flagged `false` on all three legs' first frame, while a later AU draining the DPB flagged some old trailing picture as a keyframe. That field is `DecodedImage::is_keyframe`, the pump's post-loss re-anchor signal, so a mislabel re-anchors on the wrong frame. Three changes, all inside this rung: * **A deliverable queue**, the same shape as `video_vk_native`'s — extend, ship the front, trim the oldest past the bound, count and rate-limit the drops into `DecodeHealth::dropped`. Its DEPTH is derived differently and the divergence is documented: the Vulkan rung's bound is `HOLD_HEADROOM - PIPELINE_HOLD` = 1 because a queued frame there counts against the pool ON TOP of the DPB's own residency. Here the three claims are disjoint and a bumped picture MOVES from `pending`/slot to `held`, so the queue inherits the claim rather than adding one. The bound is the DPB's depth — the deepest carry-over a bump can leave — and the measured cost is at most one surface (zero on H.264, whose three seven-picture IDR drains are the deepest bursts these vectors have). A bound of 1 would have left 235 of 250 on H.264, most of the defect still in place. * **An end-of-stream flush.** This rung has no EOS signal and cannot have one: the pump feeds access units until the session ends and then drops the decoder. So `flush` has the two honest callers — `Drop`, where nothing can be presented and the job is to release the queue's surfaces and the DPB's before the pool goes, and a caller that KNOWS the stream ended, which today is the conformance harness. One walk, not a production path and an untested teardown path. AV1 needs none: it shows at most one frame per temporal unit and buffers nothing, which its 250/250 says out loud. * **`PictureFacts` recorded when a picture decodes**, and read back when it is displayed. `keyframe` was the defect; `color` and `display` are the same mistake one field along — an in-band HDR switch changes the VUI mid-stream and AV1's render region is per-frame, so a queued frame shown two units later would have been drawn with the newest picture's signalling. Concealment answers `Ok(None)` and deliberately does NOT drain the queue, which is the Vulkan rung's order and is load-bearing: `clears_demotion_streak` is `delivered || !concealed`, so shipping a queued frame on a concealed AU would zero the streak and take away the escape hatch that stops a rung concealing forever from holding a frozen picture. The three delivered-count assertions moved with the fix, and so did the CPU derivation that reproduces them without a GPU — it now simulates the whole delivery model (ledger, queue, one-per-AU hand-off, flush) in the order `decode` does it, and carries the old behaviour beside the new one as a counterfactual: a queue bound of 0 with no flush still reproduces 225/204/45 exactly, and the test fails if it ever stops being SHORT. `settle` was split out as the pure half of `finish` so the claim walk, the display ordering and the picture facts are all assertable with no device; `the_queue_never_needs_a_surface_the_pool_does_not_have` runs the surface-lifetime arithmetic over the real vectors and pins the peak claims (9 of a 16-surface pool on H.264, 8 of 14 on both HEVC vectors), with an unbounded queue as the counterfactual that shows the bound doing its job. Gates run: `cargo fmt --all -- --check`, `cargo clippy -p pf-client-core -p pf-vaadec --all-targets --features sdl3/build-from-source -- -D warnings`, `cargo test -p pf-client-core --lib --features sdl3/build-from-source` (176 pass), the same filtered to `video_vaapi_native -- --include-ignored` (23 pass, 0 ignored) and `cargo test -p pf-vaadec` (48 pass) — all on .25 (Radeon 780M, RDNA3, radeonsi, Mesa 26.0.3, VA-API 1.23); plus `cargo fmt --all -- --check` and `cargo clippy --workspace --all-targets -- -D warnings` in pf-lxcheck2. |
||
|
|
12a5318397 |
fix(audio): place audio with the picture instead of wherever the ring settles
The host stamps `pts_ns` on every audio datagram and the client decoded it
into `AudioPacket` — and then never read it. Video's `pts_ns` is used end to
end (the presenter computes a true glass-to-glass `displayed + clock_offset −
pts`), so audio free-ran at whatever depth its jitter ring happened to reach,
video was presented on an independent path, and nothing ever compared them.
The A/V offset was an accident of buffer depths: it moved whenever the ring
ratcheted under underrun pressure, and it got WORSE every time video got
faster, because a quicker decoder lowers the video leg and leaves audio's
exactly where it was. That is what a field report on the Steam Deck heard as
"the audio delay is way too high", and it is why shaving milliseconds off the
audio budget had not helped.
Video is the master. In a game streamer the video leg is the input-feel budget
and must never be inflated to satisfy the audio clock, while audio tolerates
small crossfaded corrections that are inaudible — and `crossfade_drop` already
applies them. So audio moves:
audio_e2e = (now + buffered_ahead + clock_offset) − pts_ns
av_offset = audio_e2e − video_e2e (> 0 ⇒ audio behind the picture)
`AvSync` smooths that with an EWMA, ignores what sits inside a deadband no
listener can detect, refuses the implausible outright rather than clamping it
(a wall-clock step must not steer the ring), and proposes a depth.
Continuity outranks sync, always. `JitterPolicy::set_sync_target` only ever
takes a REQUEST, clamped between the existing underrun-driven floor and the
hard cap. A link whose jitter genuinely needs more buffer than the picture is
away keeps its buffer and the residual is reported — sync can never starve the
ring into dropouts. `None` is the default and reproduces the previous behaviour
exactly, so the four client rings can adopt this one at a time without
diverging.
Two upstream defects found on the way, both prerequisites:
* The host stamped `pts_ns` at ENCODE time, inside the loop draining an
already-accumulated chunk, so every frame of a chunk carried near-identical
timestamps describing when we got round to encoding. Harmless while nothing
consumed it; a sync loop regulating against it would regulate against a
fiction. It now comes off the capture clock.
* The host did not pace. One capture callback hands over a whole quantum — 5 ms
when the graph honours our ask, 21.3 ms on a VM, where stock PipeWire raises
`min-quantum` to 1024 — and the loop drained all of it into back-to-back
`send_datagram` calls. The wire carried a 4-5 frame burst then ~21 ms of
nothing, and a ring can only absorb that by standing a burst period deep.
Frames now leave on the audio clock, which costs no average latency.
And the reason none of this was visible: `buffer_ms`/`target_ms` existed only
as a `tracing::debug!` line, absent from `Stats`. On a Deck the client runs
under Steam's `reaper` with stdout on a pipe nobody can read, so the one number
identifying a deep ring was unobtainable on the device reporting the latency.
The HUD now carries `audio buffer N ms · a/v ±N ms` — both, because a deep ring
on a jittery link is correct and only the offset separates that from audio held
late. The host also reports its negotiated quantum against the one it asked
for, per capture open rather than once per process.
Verified: 364 core + 40 presenter tests on Linux, clippy -D warnings clean on
punktfunk-{core,host} + pf-{client-core,presenter}, fmt clean. New tests pin
the safety invariant (sync cannot pull the target below the continuity floor on
any preset), that `None` leaves the policy bit-identical, and that a device
quantum exceeding the hard cap does not panic `Ord::clamp` inside a realtime
callback.
Android and Apple keep today's behaviour (the `None` default) until their
presenters publish a video figure to align against; design/audio-latency-
overhaul.md carries the plan.
|
||
|
|
1482e6b373 |
docs(client): all four VAAPI legs have decoded — the evidence table said two never had
The H.264 and H.265 rows still read "NEVER decoded a frame on any hardware". That stopped being true on 2026-08-07, in the same session that proved AV1: every access unit of the vendored H.264 (250), H.265 (250) and HEVC Main 10 (50) vectors was accepted on .25 (Radeon 780M, RDNA3, Mesa 26.0.3) with no decode error — NV12 for the 8-bit legs, P010 for Main 10, all on the same tiled AMD modifier — and probe_this_machines_libva reports VLD decode for all three profiles. The row records the delivered counts honestly rather than rounding them up: 225/204/45 against 250/250/50 access units, because `finish` shows `outputs.last()` and drops the other pictures an AU bumps, and nothing flushes the DPB at end of stream. That is this rung's own behaviour — D3D11VA delivers all 250 — and it is invisible on punktfunk's zero-reorder host output. It is recorded and asserted rather than fixed: changing the one-frame-per-AU contract touches the pump's deliverable queue, an end-of-stream flush, and the `keyframe`-labels-the-access-unit defect in the same function, so it belongs in a commit that moves all three. Still `verified = false` for all four, and the note says why in the words the unproven-rung test requires: never frame-hash parity-checked. That is not pedantry — the D3D11VA AV1 row two lines above is a rung that decoded 250 frames and produced wrong pixels for every one of them. Parity is what distinguishes them, and this rung exports a tiled dmabuf with no CPU-readable image, so it needs a readback path nothing has written yet. |
||
|
|
79afa9ce79 | Merge branch 'fix/hevc-lowdelay-parity-gate' into integration/decode-aliasing-program | ||
|
|
bb9f482b2c | Merge branch 'fix/vaapi-decode-target-aliasing' into integration/decode-aliasing-program | ||
|
|
dc116d28ca | Merge branch 'fix/vaapi-h264-h265-hardware-proof' into integration/decode-aliasing-program | ||
|
|
d25a20a233 |
feat(vkdecode): the AV1 rungs meet a second tile for the first time
Every AV1 frame either decode rung has ever been measured against is `tile_cols = tile_rows = 1`. The vendored vector is single-tile on all 274 of its frames, so every tile array the conversions fill — `tiles.widths`, `tiles.heights`, the per-tile records — had only ever been written at index 0, and a conversion that wrote tile 0 and left the rest zero would pass the whole suite. Our encoder splits 4K into TWO TILE ROWS. **The fixture.** `lowdelay-3840x2160.ivf.av1`, 261 KB, 60 frames — `punktfunk-host spike --source synthetic --codec av1 --width 3840 --height 2160 --fps 60 --seconds 1 --bitrate 1` on .21 (NVENC, RTX 5070 Ti), wrapped to IVF with `ffmpeg -f obu … -c copy` so `common::split_av1_aus` (the vendored parser's own `IvfIterator`) frames it exactly as it frames the vector, with no second splitter that could disagree. **4K is not a size choice, it is the only shape with the property.** Measured on the same box with the same command: 1280x720, 1920x1080 and 2560x1440 all give `tile_cols = tile_rows = 1`; 3840x2160 gives `tile_cols = 1, tile_rows = 2` with `width_in_sbs_minus_1 = [59]`, `height_in_sbs_minus_1 = [16, 16]`, and both tiles in ONE Tile Group OBU. 60 frames instead of 120 pays for the resolution: 261 KB, under both the 282 KB H.264 and 270 KB H.265 low-delay fixtures. Goldens are libavcodec's software decode, cross-checked between ffmpeg n8.1.2 (Arch x86_64, libdav1d) and 8.1.1 (Homebrew, macOS arm64, libdav1d) whose 746,496,000-byte raw outputs are BYTE-IDENTICAL, not merely equal per frame. 60 of 60 digests distinct. **AV1's frame accounting is asserted, never derived.** The vendored vector is 250 temporal units carrying 274 coded frames of which 24 are hidden; this stream is 60 units, 60 coded, 60 shown, 0 hidden, 0 `show_existing_frame`, 1 key frame. Neither is the general case, so both parity harnesses now take units / decoded / shown as three independent parameters instead of computing one from another, and the CPU guard states all six numbers. **A CPU gate that needed no hardware at all.** `pic_av1`'s new `a_two_tile_frame_fills_both_row_entries_and_leaves_the_rest_zero` pins the second row entry against its OWN `height_in_sbs_minus_1`, requires the two rows to tile the frame exactly, and requires TWO tile RECORDS out of ONE tile group with rows (0,0) and (1,0) — the transposition a square grid could never reveal — each spanning real bytes. The existing one-tile test asserts index 0 is right and `1..` are zero, which a broken multi-tile conversion also satisfies. ⚠⚠ **This is a file, and on AV1 that distinction has already cost a release.** "250/250 delivered frames bit-identical to libavcodec" was true for the entire period the host was shipping only the FIRST TILE of every 4K frame: the verification ran against a vendored file while the truncation lived in packetisation, and the suite stayed green throughout. This fixture closes the multi-tile gap on the DECODE rungs and closes nothing about fragmentation, reassembly, loss or AU boundaries — the golden header, both module docs and the leg docs all say so, at length, so the next reader does not inherit the same false confidence. Legs: `low_delay_host_av1_every_frame_hashes_bit_identical_to_libavcodec` on the Vulkan rung (11 ignored legs now) and on the D3D11VA rung, plus two non-ignored CPU tests. Verified: 11/11 Vulkan parity legs on .21 (RTX 5070 Ti, 610.57.04), the new one 60/60 bit-identical; workspace clippy `-D warnings` and `cargo fmt --all --check` clean on .21. |
||
|
|
e8a7a1e6af |
fix(client/vaapi): the third rung does NOT alias — and now it cannot start to
The D3D11VA and Vulkan rungs both decoded into a surface they were predicting from, on 117 of 120 access units of our own host's low-delay H.264 (`1c54d099` for AV1, `834b2443` for H.264). `pf-vaadec` feeds `reference_frames` from the same `plan.dpb_refs` snapshot, releases its whole `removed` list inline exactly as the two broken conversions did, and neither fix commit touched it. It is still exempt — this is the evidence, and the thing that keeps it true. **Measured on the CPU, no GPU needed.** `walk_for_aliasing` drives the planner and `plan_to_va` over both streams and counts four shapes. On `lowdelay-640x480.h264` the aliasing PRECONDITION is fully present: 117 of 120 access units remove a picture their own `dpb_refs` still names, and on the same 117 the setup picture is handed the slot of a picture that access unit READS — the D3D11VA/Vulkan defect verbatim, in this conversion, today. On the vendored conformance vector both counts are 0, which is why that vector proved nothing on two other backends for two milestones. Aliased submissions: **0 on both**. **Why.** A slot is not a surface here. `plan_to_va` never invents one — every reference it can name is read out of the `surfaces` table it is handed — and the decode target is a separate parameter the caller takes from OUTSIDE that table. `setup_surface` reaches the submission at exactly one field per codec (H.264/H.265 `curr_pic.picture_id`, AV1 `current_frame` and `current_display_picture`); HEVC is doubly safe, because its per-slice `RefPicList` stores an INDEX into `reference_frames` rather than a surface. AV1's documented substitution fallback is the one place the target can be named as a reference, and only where the store resolved nothing at all to prefer. **The exemption was incidental; it is structural now.** It needs the reference table and the decode target to come from ONE snapshot of the bindings, and the rung had that only by writing `free_surface()` and `surface_table()` adjacently at three call sites. Split them and this rung acquires the defect exactly: the table must be the PRE-removal one (that is where the references are), while a free list consulted after the removals offers precisely the displaced picture's surface. `Session::acquire_target` now returns the index, the surface and the table together from `&self`, so a later edit cannot move one call and not the other. No behaviour change: same order, same values, same refusal message. Tests. `no_submission_names_its_decode_target_as_one_of_its_own_references` (both streams, 0) with `taking_the_decode_target_from_the_slot_table_aliases_on_the_low_delay_stream` as the counterfactual that reproduces the defect on 117 of 120 — so the walk demonstrably CAN see it when it is there. `the_low_delay_stream_reassigns_slots_whose_pictures_it_still_reads` pins 0/250 and 117/120 so neither can drift silently. `the_decode_target_can_never_be_a_surface_the_reference_table_names` sweeps every binding state a 4-surface/3-slot pool can hold, and `taking_the_free_surface_after_the_removals_would_hand_out_a_referenced_surface` is the ordering counterfactual. ⚠ One existing test lost a VACUOUS half. `the_setup_picture_routinely_inherits_a_just_freed_slot` asserted the decode target was never also a reference while handing every picture its own never-reused surface id — distinct integers cannot collide, so that assertion could not fail whatever the conversion did. Its real measurement (225 of 250 access units reuse a just-freed slot, which is why the target is a parameter) is kept; the collision half is gone, and the doc says where the question is actually answered and why a recycling pool is what it takes to answer it. Gates, run on `.25` (Radeon 780M, radeonsi, Mesa 26.0.3, VA-API 1.23), this rung being Linux-only: `cargo fmt --all -- --check`; `cargo clippy -p pf-client-core -p pf-vaadec --all-targets --features sdl3/build-from-source -- -D warnings`; `cargo test -p pf-client-core --lib --features sdl3/build-from-source` (171 passed); the same filtered to `video_vaapi_native` with `--include-ignored` (18 passed); `cargo test -p pf-vaadec` (48 passed). Plus the pf-lxcheck2 container for the cross-platform half — fmt, clippy and `cargo test -p pf-vaadec`, all clean. All four VAAPI legs still decode with the refactor in place, not one access unit refused: H.264 225 of 250 access units delivering a frame, H.265 204 of 250, HEVC Main 10 45 of 50 (P010), AV1 250 of 250 — the same counts and the same tiled modifier 0x200000010401b04 those legs recorded before it. ⚠ The H.26x legs live on `fix/vaapi-h264-h265-hardware-proof`, not on this branch, so they were run by overlaying that commit's test module onto the scratch tree; only the AV1 leg and the libva probe are reachable from here. This is a decode measurement, not frame-hash parity — the rung exports a driver-tiled DRM-PRIME dmabuf, so there is no CPU-readable image to hash. The alias assertions above are the real evidence and they need no device. ⚠ NOT taken: `finish`'s `outputs.last()`, which ships one frame per access unit and drops the rest of what a bump displaces (225/204/45 against 250/250/50), with no end-of-stream flush. It cannot bite punktfunk — hosts emit zero-reorder output, so `outputs` never holds more than one picture — and fixing it changes `decode()`'s one-frame-per-access-unit contract with the pump (it wants a deliverable queue, which `video_vk_native` already keeps) plus an end-of-stream flush and the `keyframe`-labels-the-access-unit defect in the same function. It is recorded and asserted on that other branch, whose three delivered-count assertions any fix has to move in the same commit; doing that from here, blind to them, would be worse than leaving it. |
||
|
|
f0702f3e06 |
feat(vkdecode): HEVC's exemption stops being an argument and becomes a vendored stream
`fd6241a2` made HEVC's freedom from the release-ordering defect falsifiable on CPU and
recorded what was still missing: no low-delay HEVC stream was vendored, so the exemption
rested on a structural argument plus one throwaway measurement. This vendors the stream,
and the exemption HELD.
**The fixture.** `lowdelay-640x480.h265`, 270 KB, 120 pictures — `punktfunk-host spike
--source synthetic --codec h265 --width 640 --height 480 --fps 60 --seconds 2 --bitrate 1`
on .21 (NVENC, RTX 5070 Ti, driver 610.57.04). Deliberately the H.264 sibling's resolution
and frame count: the two are then directly comparable, 640 and 480 are both multiples of
MinCbSizeY so there is no conformance window and a hash mismatch can only be decode rather
than readback geometry, and 270 KB sits alongside the 282 KB already accepted for H.264.
Goldens are libavcodec's software decode, cross-checked BIT-IDENTICAL across ffmpeg n8.1.2
(Arch, x86_64) and 8.1.1 (Homebrew, macOS arm64), 120 of 120 digests distinct.
**The exemption held, measured rather than argued.** `sps_max_dec_pic_buffering_minus1 = 4`
against the four pictures 8.3.2 keeps marked in steady state, `sps_max_num_reorder_pics = 0`,
`numRefL0 = 1` — a five-picture DPB filled exactly by four references plus the current
picture. 115 of the 120 access units retire a picture, and `removed ∩ dpb_refs` is **0 of
120**. A 300-picture 1080p stream from the same host reports the same shape: 295
retirements, 0 intersections. It is the encoder and not the resolution, exactly as for
H.264.
**A zero proves nothing on its own, so the fixture is pinned by its counterfactual.**
`test-25fps.h264` reported zero for two milestones while every stream we ship aliased on
99% of its frames. So the guarantee here is not "we looked and it was fine": hand
`plan_to_dxva_h265` the marked DPB as it stood BEFORE `decode_rps` — the mutation a
snapshot move would cause, reconstructed exactly as `dpb_refs(N-1) ∪ {stored(N-1)}` — and
the alias appears on **115 of 120** access units, driven through the real conversion rather
than through planner arithmetic. If a regeneration ever produced a stream that reordered,
or a DPB deeper than its reference count, that 115 collapses to 0 and the tests say so
instead of continuing to pass.
**The two rungs are exempt for different reasons, and the asymmetry is now a gate.** DXVA
binds the whole marked DPB — `RefPicList` is spec-defined that way, and an RFI long-term
anchor has to survive in it — so its exemption really is `H265Planner`'s snapshot ordering,
one call away from being untrue. `plan_to_vk_h265` never reads `dpb_refs` at all:
`pReferenceSlots` is the slots the operation uses, so it binds the current RPS sets, which
`decode_rps` itself derives and which therefore cannot name a picture that same RPS just
dropped. A new test feeds that conversion the identical widened snapshot and asserts
nothing changes, so a future change making the Vulkan rung bind the marked DPB — a
legitimate thing to want, since a *Foll* anchor invisible to the hardware is the RFI
failure shape — fails loudly instead of silently acquiring the defect.
What the Vulkan pixel leg adds is therefore NOT aliasing coverage, and its docs say so:
it is the first HEVC frame either rung has decoded from our own encoder, under a DPB that
retires and reissues a slot on 115 of 120 access units back to back, where the vendored
vector's reordering keeps that eviction slack.
Legs: `low_delay_host_h265_every_frame_hashes_bit_identical_to_libavcodec` on the Vulkan
rung (10 ignored legs now, up from 9) and on the D3D11VA rung, plus three non-ignored CPU
guards that run in ordinary CI.
Verified: 10/10 Vulkan parity legs on .21 (RTX 5070 Ti, 610.57.04), the new one 120/120
bit-identical; workspace clippy `-D warnings` and `cargo fmt --all --check` clean on .21.
|
||
|
|
fd6241a24f |
fix(dxvadec): the review round — a doc that had become false, a warn-storm on renegotiation, and HEVC's exemption made falsifiable
Four findings, all real. **`SlotMap`'s own docs had become false.** "feed it every `DpbUpdate` in decode order (via `Self::apply` or `plan_to_vk`, which applies internally)" — `plan_to_vk` no longer applies internally, which is the entire point of the change, and `release`'s docs named it as one of the two things that may free a slot. A reader following those docs would build the next caller wrong in exactly the way this commit's parent fixed. Both now say which conversions defer, which one does not, and why H.265 is the one that does not. **The deferred release warned on a legitimate event.** `release_deferred` warned per id when a deferred release found no slot — but a renegotiation replaces the whole `Session`, and with it the slot map, INSIDE `plan`, while the planner's own drain reports every drained picture in that same access unit's `removed`. Every one of those ids then misses, and nothing is wrong. `debug!`, with the legitimate cause named so the illegitimate one stays diagnosable. **HEVC's exemption was asserted only in its consequence.** `the_current_picture_is_ named_by_curr_pic_and_never_aliases_a_reference` checked that no reference shares the decode target's slot — which on the vendored vector holds whether or not the reasoning behind it does. That is precisely how the H.264 leg passed for two milestones. The test now also asserts the PLANNER property the exemption rests on (`removed ∩ dpb_refs = ∅`, falsified by moving `dpb_snapshot()` above `decode_rps`), and records that the low-delay measurement was 0 of 300 against H.264's 297 of 300 from the same host and the same run. It also records what is still missing: no low-delay HEVC stream is vendored, so HEVC's freedom is a re-derivable argument plus one measurement, not a standing hardware leg. **Two stale cross-references.** Both AV1 conversions told the reader the H.264/H.265 zero was "measured on reordering vectors and not a proof" — the open question this commit's parent closed. They now say what the answer was. |
||
|
|
834b244301 |
fix(client): the H.264 twin was real — every low-delay picture decoded into a surface it predicted from
The AV1 review round flagged the H.264 leg as "plausibly the same defect, traced in source, not reproduced" and deliberately did not touch it. It is reproduced now, and it is worse than the AV1 one: it fires on 297 of 300 access units of every stream a punktfunk host emits, at 720p, 1080p and 2160p alike, on BOTH the DXVA rung and the Vulkan one. **Decided on the CPU, no GPU needed.** `H264Planner` snapshots `dpb_refs` in `begin_picture`, BEFORE `finish_picture` runs 8.2.5's marking and C.4.5.3's bump, so a picture the sliding window unmarks and the bump then evicts lands in both `dpb_refs` (which `RefFrameList` is built from) and `dpb.removed`. The conversion released the whole `removed` list and then assigned the decode target a slot; `SlotMap::assign` takes the lowest free slot, which is the one just vacated. `CurrPic = N` and `RefFrameList[k] = N`, in one submission. The two conditions have to coincide in ONE access unit, and low-delay H.264 is exactly what makes them: `max_num_reorder_frames = 0` means the evicted picture has already been output, which is what makes it evictable at all. NVENC seals it by writing `max_num_ref_frames = 3` ALONGSIDE `max_dec_frame_buffering = 3` — a DPB exactly as deep as its reference count — so the window unmarks the oldest reference in the very unit whose bump drops it. The aliased picture is `ref_idx 2` of a three-entry `num_ref_idx_l0_active` list: addressable by any macroblock, not a spare. **Why two hardware-proven codecs and four GPUs never saw it.** `test-25fps.h264` is level 1.3 with no VUI `bitstream_restriction`, so `dpb_limit` falls back to A.3.1's level ceiling and gives a 7-frame DPB against 2 reference frames — the window unmarks two units before the bump can evict — and it REORDERS, which keeps an unmarked picture alive past the unit that unmarked it. Two independent reasons, both properties of that vector rather than of H.264. It measured zero and passed 250/250 throughout. `data/lowdelay-640x480.h264` is vendored to close exactly that: our own host's output, 120 pictures, goldens from libavcodec cross-checked bit-identical across two ffmpeg builds on two architectures. **The fix is the AV1 fix.** `DecodePlanDxva` and `DecodePlanVk` grow `release_after_decode`, the conversions hand the removals back instead of applying them, and the callers release them once the decode op is issued. It costs no slot the map does not have: `SlotMap::new` allocates `max_dpb_frames + 1` and the DPB never exceeds `max_dpb_frames`, so a free slot always exists with the whole `removed` list still held — measured, peak 4 of 4 on the stream that defers on 117 of 120 units. The Vulkan rung breaks on it in both DPB modes and neither loudly: DISTINCT hands the aliased reference the same array layer the setup writes; COINCIDE clears `slot_image[setup]` in the binding sync and the reference then resolves to no bound image, dropping out of `pReferenceSlots` with a `trace!`. Its deferred release runs on the FAILURE paths too — the fallible region's Result is held rather than `?`-ed, because seven exits sat between the conversion and the release and each would have leaked a slot. `a_full_dpb_bump_reuses_the_slot_but_the_pool_model_binds_a_fresh_image` asserted the aliasing as "the planner's normal behaviour": an authored depth-1 stream whose AU1 references the picture it evicts. It now asserts the opposite, which is the defect in two lines. New evidence, all of it runnable: the CPU proof pins BOTH numbers (0 on the vector, 117 of 120 on the low-delay stream) so neither can drift silently; the ledger-pressure test measures the peak; and a low-delay parity leg is added to `pf-vkdecode`'s `gpu_parity` and `pf-client-core`'s `video_d3d11_native::parity` so both rungs are held to what they stream rather than only to what they conform to. |
||
|
|
5aeb8d2552 |
fix(client): a failed AV1 decode left the surface's facts saying it holds the last picture
The `damaged` path has cleared `Session::held[setup_slot]` since M7, for a reason that now applies to the failure path too: the slot map says the surface holds THIS picture while the surface still carries whatever the previous occupant decoded, so a later `show_existing_frame` naming it blits the old picture's pixels with the old picture's geometry and colour. The failure path never reached that far before — `decode_into`'s error returned straight out of `frame_av1` — and the previous commit made it continue so the slot releases could run. |
||
|
|
3a4c94ad79 |
fix(dxvadec): the review round — a vacuous predicate, an overstated claim, and the H.264 twin of this defect
Five findings from the adversarial pass, all real. **The deferral predicate was vacuous.** `plan.dpb.removed` is ALWAYS a subset of `plan.dpb_refs`: `Av1Planner::plan_frame` snapshots `dpb_refs` before any mutation and `refresh_slots` can only report a picture that was in `self.slots` at that moment. So `filter(|id| dpb_refs.contains(id))` was a condition that is never false, the eager-release loop beside it could never release anything, and the test assertion "only a picture the submission points at earns the reprieve" could never fire. Now: defer every removal, say why in terms of the planner, and assert the PLANNER's property (`removed ⊆ dpb_refs`) — which is falsifiable, and whose failure would mean the conversion is releasing a surface `ref_frame_map` points at. **The failure-path claim was overstated.** Holding the decode's `Result` closes this frame's leak, not the unit's: `decode_av1` returns on the first failing frame and abandons the rest of the temporal unit's plans, so their removals are never released. 24 of 250 units carry a second frame. Named rather than fixed — what to do with the frames after a failure is the pump's question. **⚠⚠ The H.264 leg plausibly has the same defect, and the comment this change added said it could not.** `pic.rs` builds `RefFrameList` from `plan.dpb_refs`, and `H264Planner` snapshots that in `begin_picture` — BEFORE 8.2.5 marking and the DPB bump. The vendored bump drops a picture the sliding window just unmarked once it has been output, so a picture can land in both `RefFrameList` and `dpb.removed`: the AV1 aliasing shape exactly. Measured zero on the vendored vector — but that vector REORDERS, which is precisely what keeps an unmarked picture alive past the AU that unmarked it. A punktfunk host emits LOW-DELAY H.264, where output happens as each picture is decoded, which is the condition that makes eviction and unmarking land in the same access unit. Traced end to end in source, not reproduced (no low-delay vector). NOT fixed: changing a hardware-proven codec on an unreproduced suspicion is the worse risk two commits before a release. Instead `no_au_removes_a_picture_its_own_reference_list_names` makes the assumption falsifiable, and its message says what to do when it fires. HEVC is structurally safe and now says why: `H265Planner` snapshots `dpb_refs` AFTER `decode_rps`. **Four more stale promotion sites**, past the four already fixed: `Backend:: NativeD3d11va`'s variant doc, `Decoder::new`'s Windows rung comment, `lib.rs`'s module note and `clients/session/README.md`. Two sites that used the AV1 leg as the live EXAMPLE of an unproven rung are marked as expired rather than deleted — the reasoning is what the next bad-evidence leg will need. **The AV1 dump was missing.** `PF_DXVA_DUMP` wrote h264 and hevc only, for the one codec whose libavcodec capture has never been taken and where the dump is therefore the only tool. |
||
|
|
af4d265168 |
fix(client): the fourth site that swore the DXVA AV1 leg fails parity, and a clippy lint
`the_evidence_table_says_exactly_which_rungs_have_run_on_hardware` asserts the same fact a third way — a proven list and a NOT-proven list, both spelled out — so promoting the rung in the three places the handoff named still left a test saying "the DXVA AV1 leg FAILS parity on two GPUs — claiming otherwise is the dishonesty this program must not ship". It was right to fail; the pair moves lists here. Three prose sites that still described the leg as decoding wrong pixels move with it: `native_supports_av1`'s device-facts note, `log_rung`'s honesty-surface docs, and the OPEN question in the Windows Intel arm of `pick_native` — that last one is marked CLOSED rather than deleted, because the question it raised (the evidence filter asks "any evidence", and has no answer for BAD evidence) is a real gap in the rule that outlived this particular leg. |
||
|
|
a29e366b3e |
feat(vaapi): VAAPI decodes H.264, H.265 and Main 10 — their first frames on any hardware
The evidence table said these legs "have still never decoded a frame anywhere", and VAAPI is the rung every Linux AMD/Intel client lands on. They have now decoded, on `.25` (Radeon 780M / Phoenix1, RDNA3, radeonsi, Mesa 26.0.3, VA-API 1.23, /dev/dri/renderD128): H.264 250/250 access units accepted, 225 frames delivered, NV12 H.265 250/250 accepted, 204 delivered, NV12 HEVC Main 10 50/50 accepted, 45 delivered, P010 (AV1, unchanged: 250/250 accepted, 250 delivered, NV12) all on the same tiled AMD modifier (0x200000010401b04). Not one access unit of any vector was refused. Three `#[ignore]`d legs modelled on the AV1 one, plus the Annex-B access-unit splitters they need — ported verbatim from `video_d3d11_native`'s test module so the two platform rungs are driven over the same access units rather than over two splitters free to disagree. Main 10 earns a third leg rather than a variation on the second: ten bits is a different VAAPI profile, a different render-target format and a different surface fourcc, and that leg's fourcc assertion is the only thing that would catch a driver quietly handing back NV12 for a ten-bit stream. This is NOT frame-hash parity, and the doc comments say so rather than letting the test names imply it. The Vulkan and D3D11VA legs hash every frame against libavcodec because both can read their decoded surface back; this rung exports a DRM-PRIME dmabuf whose memory the driver tiles, so there is no CPU-readable image to hash without a `vaDeriveImage`/`vaGetImage` path production neither uses nor wants. What these legs prove is that every access unit is accepted, that the expected number of frames comes back, and that each one is a real exported surface of the right shape and fourcc — enough to turn "never decoded a frame anywhere" into a measurement, not enough to promote the rung to `verified`. Two findings the run surfaced, neither of which bites punktfunk's own streams: * The delivered counts are 225/204/45, not 250/250/50, and that is the RUNG, not the driver. `finish` shows `outputs.last()` and never more, so an access unit whose plan bumps several pictures out of the DPB displays the last and drops the rest — 18 dropped at the H.264 vector's three draining IDRs, 45 on the H.265 vector's 45 two-picture bumps — and there is no end-of-stream flush. Hosts emit zero-reorder low-delay output with no B pictures, so `outputs` never holds more than one picture in the field. A CPU-only test derives all three counts from the planner alone, on any Linux box with no GPU, so they stay explanations rather than recordings. * `DmabufFrame::keyframe` labels the ACCESS UNIT, not the picture delivered: `finish` is handed the current AU's `is_idr`. On a reordering stream the IDR is bumped out several access units after it decoded and arrives flagged `false`, while the access unit that drains the DPB at a later IDR flags whichever old picture it displays as a keyframe. That flag is `DecodedImage::is_keyframe`, the pump's post-loss re-anchor signal. Asserted so that fixing it is noticed, not so that it is preserved. Gates, all run on `.25` (this rung only compiles on Linux): `cargo fmt --all -- --check`; `cargo clippy -p pf-client-core --all-targets --features sdl3/build-from-source -- -D warnings`; `cargo test -p pf-client-core --lib --features sdl3/build-from-source` (169 passed); the same filtered to video_vaapi_native with `--include-ignored` (16 passed). Plus the pf-lxcheck2 container's workspace-wide `cargo fmt --all -- --check` and `cargo clippy --workspace --all-targets -- -D warnings`, both clean. The evidence table in `video.rs` still says these legs have never decoded a frame. It is being edited concurrently, so its replacement row is handed over rather than raced for here. |
||
|
|
f4dda9074b |
feat(dxvadec): the AV1 picparams harness AV1 forgot, and the D3D11VA AV1 rung is promoted
Two halves. **The harness.** `libav_picparams_parity` covered H.264 and HEVC only, which is exactly the gap that let a wrong AV1 submission ship. It now plans, converts and packs all 274 frames of the vendored AV1 vector and checks what needs no capture: the three-buffer descriptor set with no quantization matrix (AV1's matrices are selected by index, so `dxva2_av1_end_frame` passes NULL/0 and there is no buffer to submit), no macroblock count anywhere, the 912-byte picture-parameter buffer, and the tile records — which unlike H.264/HEVC slice records do NOT abut, because a `DXVA_Tile_AV1` addresses a tile PAYLOAD and consecutive payloads are separated by their `tile_size_minus_1` fields. The one that matters most is `no_av1_submission_names_its_decode_surface_in_the_ reference_store`: the invariant the previous commit fixed, over the submitted BYTES rather than over the plan. libavcodec cannot produce that shape — it fills `RefFrameMapTextureIndex` from the pre-refresh store and takes `CurrPicTextureIndex` from a frame the reference update has not run on — which is the argument for calling it a defect rather than a convention. `AV1_FIELDS` reaches into the eight nested blocks (`tiles.widths`, `segmentation.feature_data`, …) so a future capture reports a field and not "260 bytes of tiles differ"; `field_table!` grew nested-path support for it. The `#[ignore]`d `our_av1_picture_parameters_match_libavcodecs` and the capture recipe are in place, and `the_dump_and_the_parser_agree…` now self-compares AV1 too. ⚠ NO libavcodec AV1 capture was taken and the module docs say so rather than leaving an absent result to be read as a pass: `.221` has no MSYS2, no gcc and no make, so a patched FFmpeg there is a toolchain bring-up, not a build. Everything this file claims about libavcodec's AV1 side is READ out of `dxva2_av1.c` (n8.1). That reading did turn up one live divergence, recorded at `pic_av1.rs`'s `pp.width` and deliberately NOT changed: libavcodec sends `avctx->width`, which is FrameWidth (pre-superres), where this crate sends UpscaledWidth. The two are equal whenever superres is off, which is every stream that exists here, so the 250/250 result says nothing either way and a blind change would be unmeasured. **The promotion.** `(D3d11va, CODEC_AV1)` is `verified` — 250/250 delivered frames bit-identical to libavcodec on an RTX 3500 Ada AND an Intel Arc. All three places move together: the evidence arm, the module table and `every_rung_runs_and_the_unproven_ones_are_named`, whose `unproven` array loses the pair and whose proven list gains it. ⚠ This changes rung SELECTION, not just a label. `verified` is what lets `auto` pick D3D11VA ahead of Vulkan Video, so Windows Intel and unknown-vendor boxes — where the ladder is `native-d3d11va → native-vk → sw` — now decode AV1 on D3D11VA where they previously fell to Vulkan. Taken deliberately: ~10x the Vulkan leg's speed, and the parity that promoted it was measured on an Intel Arc, which is the vendor family the change moves. Still no soak on the goldens, and the notes say so. Also: `frame_av1` holds the decode's `Result` instead of `?`-ing it, so both slot releases run on the failure path. `decode_av1` notes an error and keeps the session rather than rebuilding the slot map, so an early return leaked a surface per failed frame and hit `SlotError::Full` after nine. |
||
|
|
1c54d0999b |
fix(client): the D3D11VA AV1 rung decoded every inter frame into a surface it was predicting from
AV1 applies `refresh_frame_flags` AFTER the frame is decoded (7.20), so a frame that reads a reference slot and then overwrites it is the ORDINARY case, not an exotic one: 268 of the vendored vector's 274 frames do it, first at frame 6. `plan_to_dxva_av1` released every displaced picture inside the conversion — which is what the H.264 and H.265 siblings do with their whole `removed` list — and then assigned the decode target a slot. `SlotMap::assign` takes the lowest free slot, and the lowest free slot is the one just vacated. So the submission said `CurrPicTextureIndex = N` and `RefFrameMapTextureIndex[k] = N` in the same breath, on 268 of 274 frames: decode into the surface you predict from. Neither vendored H.264 nor H.265 vector ever produces that shape (measured: zero on 250 AUs), which is why an eager release survived two hardware-proven codecs and opened on the first AV1 frame past the key frame's neighbourhood. HEVC even has the invariant under test already — `the_current_picture_is_named_by_curr_pic_and_ never_aliases_a_reference` — and AV1 had nothing. The Vulkan rung already carries the fix; this is the same contract, and the DXVA constraint is the STRICTER of the two: Vulkan binds only the references a frame names, while `RefFrameMapTextureIndex` declares the whole store, so every picture the store still names has to survive the conversion. `DecodePlanDxvaAv1` grows `release_after_decode` and `frame_av1` applies it once the decode op is issued — next to the `refresh_frame_flags == 0` release that already waits for the same reason. Peak surfaces held goes 7 of the 9 the pool allocates, so the spare slot `SlotMap::new` adds is doing exactly the job it exists for. Measured on hardware before the fix: Intel Arc got 245 of 250 delivered frames wrong — 47% of luma at the first bad frame, max |delta| 242, chroma wrong too, a frame predicted from the wrong picture — and the only late frame it got right was the one intra frame, which names no reference and so could not alias. That reads as a `primary_ref_frame` defect and is not one: PRIMARY_REF_NONE and "has no references to alias" are the same frames. |
||
|
|
6d0a389dd2 |
fix(client): the D3D11VA AV1 rung decodes wrong pixels — the parity harness existed all along
The follow-up was framed as "build the frame-hash parity harness the D3D11VA AV1 rung is missing, then flip hardware_verified to true". Both halves were wrong. The harness was never missing. `video_d3d11_native`'s `parity` module has carried `av1_every_delivered_frame_hashes_bit_identical_to_libavcodec` since M7 wired the rung — wired to the SAME libavcodec goldens the Vulkan AV1 leg passes against, with the display-order model that handles the vector's 24 hidden frames, sitting `#[ignore]`d beside the H.264/H.265/Main10 legs. It had simply never been run on a device; .173 was powered off the day it was written. What the old evidence note called a missing harness is real about pf-dxvadec the CRATE, which cannot host one — it links no D3D11 — but the device half lives here and was already done. Run on .221, it FAILS, on both GPUs, deterministically (three runs each, identical first-divergent frame and identical hashes): 186/250 diverging display frames on an RTX 3500 Ada, 245/250 on an Intel Arc. It is the decode that is wrong, not the measurement, and three independent checks say so. H.264 and H.265 pass 250/250 and HEVC Main 10 50/50 through the same harness, the same readback geometry, the same crop and the same slot map on those same two GPUs. pf-vkdecode's Vulkan AV1 leg reproduces the same golden file 250/250 on the same box. And the goldens regenerate byte-for-byte from the ffmpeg build their own header names. Two signatures, and they are not one defect wearing two faces. NVIDIA is bit-exact for display frames 0..=63 and then loses ONE 16x24 luma block — 174 pixels, max |delta| 8, chroma untouched — on the frame whose order_hint first reaches 64, after which every remaining frame is downstream of it through prediction. The stream parks the key frame (order_hint 0) in BWDREF and ALTREF2 for its whole length, so 64 is where the distance to it reaches the edge of what get_relative_dist can represent at OrderHintBits = 7. Intel is structurally wrong from display frame 4 — 47% of luma, max |delta| 242, chroma wrong too, a frame predicted from the wrong picture — and the only later frame it gets right is the one whose primary_ref_frame is PRIMARY_REF_NONE. None of this is visible on glass, which is the whole argument for goldens: the rung streams 4K60 on both parts with a clean five-minute soak at roughly ten times the Vulkan leg's decode time. The 2026-08-07 field sessions that looked clean were looking at wrong pixels. So hardware_verified stays false, and the note now says why in the strongest available terms — it prints at warn on every session that lands here, and "decodes AV1 to wrong pixels" is what a support engineer needs to read. The pair stays in `every_rung_runs_and_the_unproven_ones_are_named`'s unproven array; its note still contains NEVER, because the pair has never PASSED parity, which is now a measured statement rather than an absence. Left deliberately unchanged: `auto` on Windows can still reach this rung for AV1, and on Intel it is the arm that fires, because that vendor advertises no SAMPLED usage on any decode profile so zero-copy Vulkan Video cannot run there. Barring it trades visibly-wrong AV1 for the software rung, which cannot keep up at 4K and is itself unproven. Which way that trade goes is a product call, so it is recorded at the admission site rather than made silently here. `av1_divergence_map` is kept, cleaned up and documented: it is what turned "186 frames differ" into a lead — one line per display frame, its verdict beside the plan facts that could explain it, and an opt-in raw-NV12 dump. At a frame where one vendor hashes correctly, that vendor's bytes ARE libavcodec's bytes and so a valid reference for the other's, which is how "how badly" was answered without new goldens. The tool that would localise the rest does not exist: pf-dxvadec's libav_picparams_parity covers H.264 and HEVC only, so the AV1 conversion has never been compared against libavcodec at the picture-parameter level either. That is the next step, not another session. Also in this file, since it is the same table and the same day: the VAAPI rung's AV1 leg has now decoded 250/250 of the vendored vector on RDNA3 and its arm is split from the H.264/H.265 ones, which genuinely have still never decoded anything. It is unverified for the same reason as ever — no parity — and the D3D11VA row above is exactly why that distinction is worth keeping: a rung can decode 250 frames and still be wrong. |
||
|
|
f351eb01e9 |
feat(vaapi): VAAPI decodes AV1 — the rung's first frame on any hardware
The evidence table has said "native VAAPI: has never decoded a frame anywhere
(M6/M7)" since the rung was written. That is no longer true. Measured on `.25`
(Radeon 780M / Phoenix1 RDNA3, radeonsi, Mesa 26.0.3, VA-API 1.23, Ubuntu
26.04 — headless, no display server needed):
VAAPI AV1 rung constructed: native-vaapi av1
VAAPI AV1: 250 frames delivered, first 320x240 fourcc="NV12"
modifier=0x200000010401b04
250 of 250 displayed frames, first try, on the same vendored vector the Vulkan
and D3D11VA AV1 legs walk. The count matters as more than a smoke test: the
vector carries 274 coded frames in 250 temporal units — 24 units carry two, and
those extras are HIDDEN (decoded, referenced, never shown) — so 250 delivered is
this rung agreeing with the other two about which frames are output. A tiled AMD
DRM modifier rather than a linear one says the surface is a real decode target,
not a fallback.
Two changes, both in the rung's own file.
**The probe never asked about AV1.** `probe_this_machines_libva` walked H.264
High, HEVC Main and HEVC Main 10 and stopped there, which is part of why "never
decoded a frame" could stand so long without anyone noticing what had not been
asked. It now covers both AV1 profiles, and this box answers:
H.264 High: VLD decode AV1 Profile 0: VLD decode
HEVC Main: VLD decode AV1 Profile 1: no (VAProfile not supported)
Profile 1 being refused is correct — 4:4:4 AV1, which radeonsi does not do — and
it is the negative case that proves the probe reports rather than assumes.
**`av1_decodes_the_vendored_vector_on_this_machines_vaapi`** is the decode
itself, `#[ignore]`d beside the probe.
It is deliberately WEAKER than the Vulkan and D3D11VA AV1 legs, and the docs say
so rather than letting the name imply parity: those two hash every frame against
libavcodec's goldens because both can read their decoded surface back. This rung
hands out a DRM-PRIME dmabuf whose memory the driver tiles, so there is no
CPU-readable image to hash without adding a vaDeriveImage/vaGetImage path that
production neither uses nor wants. So it asserts what can be asserted honestly —
every temporal unit accepted, the right number of frames back, each a real
exported surface of the right shape, the first flagged as a keyframe — and it is
NOT frame-hash parity. Promoting this rung to `verified` still wants parity, and
parity wants a readback path first.
It fails loudly rather than skipping when the device has no AV1 entry point. It
is `#[ignore]`d, so it only runs when someone points it at a box that is supposed
to have one, and a silent pass there is exactly the invisible-failure mode this
program exists to end.
Gates: on `.25`, fmt clean, `clippy -p pf-client-core --all-targets -D warnings`
green under the Linux cfg where this rung actually compiles, the whole lib suite
167/167, and all 11 VAAPI tests green with `--include-ignored`. Workspace fmt +
clippy + lib suite also green in the Linux container.
⚠ Not touched here on purpose: the evidence table in `video.rs`. Its VAAPI row
still reads "never decoded a frame anywhere" and now understates what is known —
but a parallel agent is editing that same file for the D3D11VA AV1 row, so the
row is left for whoever lands second to update once, rather than conflicting.
Note for anyone reproducing on `.25`: it has no system SDL3 and no passwordless
sudo, so the test binary links only with `--features sdl3/build-from-source`
(SDL3 is gamepads, irrelevant to decode; production Linux still links the system
one). Its disk sits at ~99% full, and the tree there is a `git archive` export
with no `.git`, so `git apply`/`git checkout --` silently do nothing.
|
||
|
|
5a4305c072 |
merge: bring current main into the gyro correctness branch
main moved ~60 commits while this branch was in progress, and one of them matters here: PR #88 (the phone-gyro mirror) landed, touching the same motion path. One conflicted file, `GamepadCapture.swift`, in three places — all of them the two changes meeting rather than disagreeing: - **Slot fields.** #88 added `motionSent` + `lastAccel` for its flush-parks-motion fix; this branch removed `lastMotionNs` with the 4 ms drop-throttle. Kept both decisions: the parking state stays, the throttle field goes. - **forwardMotion's head.** #88 added the mirror stand-down (`pad 0` yields while the phone speaks for it); this branch deleted the throttle guard. Kept the stand-down, dropped the guard. - **The send.** This branch converts into the DualSense report frame; #88 records what went out so `flush` can replay it beside a zero gyro. Both, with the recording placed AFTER the conversion — `flush` replays `lastAccel`, so it has to be the vector that actually went on the wire, or a still pad's gravity gets parked in the wrong axis. The two features compose exactly, which is worth stating because it is not luck: this branch gates motion capture on `hasRotationRate`, and #88 engages the phone mirror when `hasRotationRate != true`. They are complements — a pad either drives its own gyro or the phone mirrors for it, never both and never neither. Everything else auto-merged. Note `DeviceGyroRemapTests` is `#if os(iOS)`, so the macOS suite reports the same 215 as before the merge rather than gaining #88's six — checked, not assumed. Gates re-run against the merged tree rather than trusting either side's: Linux fmt + build + `clippy --locked --all-targets -D warnings` + punktfunk-core and pf-inject suites; Apple 215 tests and the iOS-triple typecheck; Android kit + app compile and tests. All green. |
||
|
|
19c9165d4b |
docs(client): the D3D11VA AV1 rung has two vendors and a soak now — and still no parity
Re-measured against a host carrying #95, from .21 (RTX 5070 Ti, av1_nvenc) to .221, on glass: Intel Arc, auto -> native-d3d11va 4K60, decode 1.4 ms, e2e 16.7 ms p50 RTX 3500 Ada, pinned native-d3d11va 4K60, decode 1.0 ms RTX 3500 Ada, pinned native-vulkan 4K60, decode 11.6-16.7 ms Plus a 5-minute Arc soak: 297 stats lines, 60 fps, decode 1.3 ms, e2e 10.9/14.8 ms p50, and exactly one WARN in the whole run — the hardware_verified=false notice itself. No refusals, no demotions, no concealed runs. Three things that follow. The rung is no longer a one-session curiosity: it decodes 4K60 AV1 on TWO vendors and survives a soak. The Arc leg matters twice over, because the Arc advertises no SAMPLED usage on any decode profile — zero-copy Vulkan Video cannot work there — so `auto` demoting to D3D11VA and then decoding is the whole demotion path working as designed. It is roughly 10x faster than the Vulkan AV1 leg on the SAME NVIDIA GPU. That is the strongest argument yet for eventually letting `auto` pick it ahead of Vulkan Video, which is exactly what `verified` gates. And it stays `verified = false` anyway, because the missing piece is specific: there is no frame-hash parity against libavcodec. Every other verified pair in that table earned it with one, and pf-dxvadec has no harness that could produce one — `libav_picparams_parity` compares picture parameters on the CPU and never decodes a frame. Building that harness is the work that promotes this rung; a fourth session is not. The evidence string now says so, so the next reader does not have to rediscover which half is missing. The VAAPI row is corrected in the same spirit rather than left as a bare "NO": the reachable VAAPI box (.25, RDNA3) reports VAProfileAV1Profile0 / VAEntrypointVLD and advertises no Vulkan AV1 decode at all, which makes it the right box to prove that rung on and an unambiguous oracle when it happens. What stopped it is recorded too — no punktfunk checkout there and 4 GB of usable RAM. Documentation only — no behaviour change, and no flag flipped. |
||
|
|
c64cdc4ef7 |
docs(encode): close out the tile-aware AV1 sub-frame reader — measured, not worth it
#95 disarmed sub-frame readback for AV1, which means AV1 forgoes the latency win HEVC gets from shipping slice 1 while slice 2 encodes. The follow-up was to teach the reader AV1's units: cut on OBU boundaries rather than byte counts and arm from the driver's reported unit count. Measured on .21 (RTX 5070 Ti, av1_nvenc) before writing any of it, and the measurement closes it rather than scoping it. Reading the frame headers av1_nvenc actually emits at 4K: width_in_sbs_minus_1[0] = 59 one tile column, the full 3840 height_in_sbs_minus_1[0..1] = 16, 16 two tile rows tile_start_and_end_present_flag = 0 BOTH TILES IN ONE TILE GROUP OBU That last flag is the finding. "Cut on OBU boundaries" presumes the tiles are separate OBUs and they are not — there is no boundary between them to cut on. Shipping tile 1 early would need the HOST to re-author AV1 syntax per chunk, synthesising a fresh Tile Group OBU header with tile_start_and_end_present_flag = 1 and its own tg_start/tg_end. That is bitstream surgery on the encode path, not the reader change it was assumed to be. And the prize would be small even then, because split encode already spent it. The two tile rows go to two split-encode engines that run CONCURRENTLY, so they complete at nearly the same moment — the win is bounded by the skew between engines, not by half a frame. Whole-frame encode measures 3.3-3.6 ms at 4K60 against a 16.7 ms p50 end-to-end, so even the sequential-tiles fantasy caps near 1.7 ms and the real number is a fraction of it. HEVC's win is bigger for a structural reason that does not transfer: forced split and sub-frame are mutually unsupported, so HEVC's slices genuinely are produced one after another. 1080p settles it further: tile_cols_log2 = tile_rows_log2 = 0, a single tile, so there is nothing to pipeline at the commonest streaming resolution at all. Recorded next to the disarm with the reopen condition named — NVENC emitting one OBU per tile, or setting tile_start_and_end_present_flag = 1 — so this is closed on evidence rather than left as an open maybe. Documentation only — no behaviour change. |
||
|
|
6b4be28d24 |
docs(client): write down why the CPU rung is not process-isolated
#97's frame-context floor closes the one rav1d abort we hit and can prove. It does not make the rung panic-proof and nothing at that call site can, because rav1d's public surface is dav1d's C ABI: any reachable panic crosses `extern "C"` as `panic_cannot_unwind` and becomes `abort()`, past every `catch_unwind`, rung demotion and typed refusal we have. Counted across rav1d 1.1.0's 60 source files: 285 `unwrap()`, 214 `assert!`, 19 `unreachable!`, 11 `expect()`, 10 `panic!`. 539 sites that end the client if a stream can reach them. #97 fixed one of them. Process isolation is the only defence that actually works, and this records the decision NOT to build it, with the reasoning, so it is not re-argued from scratch each time someone reads that number: * the defect is upstream's and is one line (memorysafety/rav1d#1497, filed 2026-08-07 with the fix and a reproducer; still open, no PR, as of today); * 539 is an unbounded number, not a risk estimate — none of those sites is known reachable from a punktfunk stream, and the honest next step is to fuzz the rung and find out, which is cheap, rather than buy insurance, which is not; * the cost lands on the video path across Linux, Windows and Android (the Apple clients decode through VideoToolbox and never reach this code), each needing its own shared-memory frame transport, child lifecycle and backpressure, and it adds a scheduling boundary to the slowest rung on the ladder while zero-copy is a hard requirement; * an abort here costs a session that was already degraded — this rung exists because the GPU rungs failed first. The trigger to revisit is named as an event rather than a feeling: a SECOND distinct abort in the field, or a fuzzer finding a reachable panic. Either makes it a class of bugs instead of one, and a class is what would justify the architecture. Documentation only — no behaviour change. |
||
|
|
669176982d |
fix(h264): name the DPB cliff #96 left standing in the other codec
H.264 derives its DPB size the same way HEVC did before #96 — from a level ceiling that says what a stream MAY use, not what it needs — and the ceiling saturates at 16 frames, which is 17 hardware slots with the picture in flight. That is the exact arithmetic that cost 720p and 1080p their HEVC. Measured on real encoders (2026-08-07) rather than assumed: H.264 escapes it twice over, and both escapes belong to the encoders, not to the format. encoder level picked VUI restriction NVENC (RTX 5070 Ti, 610.57.04) 3.2/4.2/5.1/5.2 present, buffering 3 VAAPI via libavcodec (RDNA3, 26.0.3) 4.1/4.2/5.1/5.2 present, buffering 1 openh264 (the software rung) 3.2/4.2/5.1/5.2 present, buffering 1 Every one picks a level proportionate to the picture AND states its real need in the VUI bitstream restriction, so the ceiling is never reached and never consulted. Nothing is broken today, and clamping would be wrong: with the restriction present the number IS the stream's own statement, and a stream that genuinely asked for a deep DPB would decode wrong if we shrank it. So this does not change what any stream decodes. It gives the arithmetic one named home (`dpb_limit`, the twin of `h265::dpb_limit`) carrying the evidence and the reasoning, and it adds the signal that was missing: when an SPS carries no restriction AND its level ceiling would demand more slots than mainstream hardware provides, the plan now says so with `PlanWarning::LevelDerivedDpb` instead of a user silently losing the codec the way #96's users silently lost HEVC. It is not an integrity warning — the picture is intact; what fails is opening a session — so `is_integrity_warning` classifies it false. One thing the sweep corrects about how the follow-up was framed: it is SMALL pictures that saturate the ceiling most easily, not 720p specifically. 640x360 at level 3.1 computes 16 as readily as 720p at level 5.0, because the ceiling is MaxDpbMbs divided by the picture's macroblocks. The authored 64x64 test fixtures land there too, which is why they now assert through `picture_warnings`. Guards, as the missing consumer-end half of pf-encode's `rfi_dpb_fits_a_mainstream_vulkan_decoder`: * every_reachable_h264_stream_fits_a_mainstream_slot_pool — the measured (picture, level, declaration) pairs, asserting slots <= 16 * the_level_ceiling_alone_would_reproduce_96_and_is_warned_about — the same resolutions at levels that saturate, pinned WITH the warning * a_proportionate_level_fits_even_without_a_vui_restriction — so neither escape looks like it is doing all the work alone Gates: fmt + clippy -D warnings clean; pf-client-core 167/167; pf-bitstream 84/84; and gpu_parity 8/8 bit-identical to libavcodec on the RTX 5070 Ti, which is the gate that matters for anything touching the bitstream layer. |
||
|
|
d996449a82 |
fix(host/pads): a virtual pad at rest said it was in free fall
G14, unblocked by the frame measurement in
|
||
|
|
a8a4b11f5c |
Merge pull request 'HEVC died at 720p and 1080p because we sized the DPB from the level's ceiling, not the stream's need' (#96) from fix/hevc-dpb-level-ceiling into main
ci / web (push) Successful in 1m6s
ci / bun-nix (push) Successful in 50s
apple / swift (push) Successful in 1m32s
ci / docs-site (push) Successful in 1m15s
ci / rust-arm64 (push) Successful in 2m46s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
ci / rust (push) Failing after 4m2s
deb / build-publish-client-arm64 (push) Successful in 3m0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m4s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m18s
deb / build-publish (push) Successful in 5m30s
android / android (push) Successful in 7m28s
docker / builders-arm64cross (push) Successful in 10s
deb / build-publish-host (push) Successful in 5m58s
apple / screenshots (push) Successful in 6m1s
docker / deploy-docs (push) Successful in 32s
arch / build-publish (push) Successful in 10m30s
flatpak / build-publish (push) Successful in 7m11s
windows-host / package (push) Successful in 14m24s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 22s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m37s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m30s
Reviewed-on: #96 |
||
|
|
8805a18375 |
Merge pull request 'A damaged AV1 frame stops killing the whole client (and a BOM stops erasing every setting)' (#97) from worktree-rav1d-single-frame-context-abort into main
android / android (push) Canceled after 0s
apple / swift (push) Canceled after 16s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 4s
deb / build-publish-client-arm64 (push) Canceled after 4s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 4s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #97 |
||
|
|
e9ddc3c561 |
Merge pull request 'AV1 stops shipping half a frame' (#95) from worktree-av1-subframe-truncation into main
android / android (push) Canceled after 22s
apple / swift (push) Canceled after 57s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 46s
ci / rust (push) Canceled after 54s
ci / rust-arm64 (push) Canceled after 28s
ci / docs-site (push) Canceled after 14s
ci / web (push) Canceled after 21s
ci / bun-nix (push) Canceled after 15s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 39s
deb / build-publish-client-arm64 (push) Canceled after 35s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 17s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 17s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 9s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 8s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 32s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
windows-host / package (push) Canceled after 2m28s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
Reviewed-on: #95 |
||
|
|
8551e88fcb |
merge: bring current main into the audio-substrate branch
ci / bun-nix (pull_request) Successful in 47s
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m27s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m27s
ci / rust-arm64 (pull_request) Successful in 4m25s
android / android (pull_request) Successful in 5m59s
ci / rust (pull_request) Successful in 7m19s
Two conflicts, both unions of independent removals/fixes: main fixed the same three install.rs SAFETY comments this branch fixed (main's phrasing kept), and the runner provisioning drops BOTH env lines — main removed PF_FFVK_VULKAN_INCLUDE (pf-ffvk is gone since the FFmpeg replacement), this branch removed VBCABLE_DIR (the retirement). |
||
|
|
bca63cd9ff |
docs(client/video): name the upstream rav1d issue next to the workaround
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m53s
ci / bun-nix (pull_request) Successful in 25s
ci / docs-site (pull_request) Successful in 1m23s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m36s
ci / web (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 3m41s
ci / rust (pull_request) Successful in 6m21s
memorysafety/rav1d#1497, filed with the one-line fix and a reproducer that needs no capture — any AV1 stream with one temporal unit removed. Written down where the setting is, because the next person to read `av1_settings` and wonder whether the floor is still needed should be able to check rather than re-derive it. |
||
|
|
eb9203b115 |
fix(client/settings): a byte order mark stops silently erasing every setting
ci / bun-nix (pull_request) Successful in 40s
ci / web (pull_request) Successful in 1m14s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m12s
ci / docs-site (pull_request) Successful in 1m27s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m56s
android / android (pull_request) Canceled after 2m30s
ci / rust (pull_request) Canceled after 2m17s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 1m8s
`Set-Content -Encoding UTF8` writes a UTF-8 BOM, and every Windows how-to
reaches for it, so `%APPDATA%\punktfunk\client-windows-settings.json` edited
from a shell arrives with `EF BB BF` in front of the `{`. serde_json rejects
that at byte 0 — correctly, JSON has no BOM — and
`.and_then(|s| serde_json::from_str(&s).ok())` turned the refusal into
`Default`. Every setting in the file, gone, with the file plainly correct on
screen and not one word anywhere about why.
Cost an hour on 08-07: a `codec: "av1"` edit was ignored and the client
negotiated HEVC. The obvious suspects — the negotiation, the caps, the host —
were all working exactly as designed.
So the mark is stripped, which is what every other JSON consumer on Windows
does. But the BOM is only the instance; the bug is the `.ok()`, which hides a
trailing comma, a truncated write and a hand-edit typo just as completely.
Those now cost one `warn!` naming the file and serde's own line and column. A
file that cannot be READ at all is reported too, and for the same reason: PowerShell's
`-Encoding Unicode` writes UTF-16LE, `read_to_string` rejects it as invalid
UTF-8, and that lands in exactly the same hole.
The RESULT is deliberately unchanged — `Default`, never an error. Nothing about
streaming may hinge on a settings file being readable, and refusing to start
because one is malformed would be a worse failure than the one being fixed. A
missing file stays silent, because that is just first run.
All three of this client's JSON stores share the loader, because all three had
the identical line: the settings file, the known-hosts store (where a BOM
silently unpairs every host) and the profiles catalog.
|
||
|
|
32ff93199f |
fix(client/video): a damaged AV1 frame stops killing the whole client
The software rung aborted the process — not the session, the process — the
first time a 4K AV1 stream lost a frame. Reproduced on .21 twice on 08-07,
`SIGABRT` a few hundred milliseconds after "first frame decoded".
It was never about 4K, and it was never our bitstream.
rav1d 1.1.0 kills the process on ANY decode error while it holds a single
frame context. `rav1d_submit_frame`'s `c.fc.len() == 1` branch calls
`rav1d_decode_frame` inline; that always finishes in
`rav1d_decode_frame_exit`, which does an unconditional
`mem::take(&mut f.frame_hdr)` (decode.rs:4873); and then, only if the decode
returned `Err`, the same branch re-enters a local `on_error` whose first act is
`f.frame_hdr.as_ref().unwrap()` (decode.rs:4997) — on the `None` the teardown
just left. The panic unwinds into `dav1d_send_data`, which is `extern "C"`, so
it is `panic_cannot_unwind` → `abort()`: no `catch_unwind` at our call site, no
rung demotion and no `NoSoftwareRung` refusal can catch it. The same code is in
upstream `main` today, and 1.1.0 is the newest release, so there is no version
to bump to.
4K was only where an error first HAPPENED. The CPU rung cannot keep up at
3840x2160 (35-39 fps against a 60 fps stream), so the receive backlog stopped
draining, `pump::data` flushed it and jumped to live, and the next AU
referenced frames nobody had decoded. libdav1d gives the identical verdict on
the identical capture — 13 frames, then "Invalid data found when processing
input" — and simply carries on. At 1080p the rung keeps up, nothing is ever
flushed, no AU is ever damaged, and the same code ran for years without
anybody seeing this.
So the fix is to stop asking rav1d for the configuration whose error path is
broken. `c.fc.len() > 1` never calls `rav1d_decode_frame` at all: it hands the
frame to `rav1d_task_frame_init` and errors come back through `cached_error` /
`task_thread.retval` as ordinary `EINVAL`s, which the pump already answers with
a keyframe request. Measured, against the captured 4K stream:
n_threads=8 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=2 -> n_fc=1 -> ABORT <- proves the rule
n_threads=8 max_frame_delay=2 -> n_fc=2 -> 13 pictures, EINVAL, survives
n_threads=8 max_frame_delay=0 -> n_fc=3 -> survives
The third row is why `n_threads` grows a floor of two as well as the delay:
`n_fc` is `min(max_frame_delay, n_threads)`, so one decode thread silently puts
the whole thing back on the aborting path. That row is also what rules out the
theory this investigation started with — pinning threads to 1 was the suspected
trigger, and it makes things WORSE, so the tile workers are innocent and the
single frame context is the entire defect.
Two frame contexts would normally cost a frame of latency, and this does not,
because `decode` now drains PAST the first `EAGAIN`. `rav1d_get_picture` only
reaches its blocking `drain_picture` on a call whose own `drain` flag is already
set, and that flag is set by the PREVIOUS `get_picture` and cleared by every
`send_data` that carried bytes — so the first `EAGAIN` after a send does not
mean "no picture for this AU", it means "ask again", and this AU's frame comes
out of the second call. Stopping at the first `None` is what a
single-frame-context reading of dav1d's API teaches, and it would have put the
pipeline two frames behind while looking perfectly healthy. Measured over 14
temporal units at `n_fc = 2`: stopping at the first `None` produces nothing at
all for units 0 and 1; draining past it produces one frame per unit from unit 0,
at 20-42 ms per unit against `n_fc = 1`'s 21-53 ms. Not a trade — same cadence,
slightly faster, because the tile workers overlap the drain.
`Av1Software::new` then asks rav1d itself, through `dav1d_get_frame_delay`,
what those settings actually bought, and refuses to open a decoder that would
run with one frame context. That is not a restatement of the arithmetic: it is
`get_num_threads`' own answer, so it stays right if rav1d's derivation changes.
It is there because the failure it guards is uniquely quiet — an edit that
reinstates `n_fc = 1` costs nothing at build time, nothing in the tests and
nothing on a clean link, and then kills the client the first time a frame
arrives damaged. Losing the rung is recoverable; `abort()` is not.
On glass, .21, 35-second sessions, `PUNKTFUNK_DECODER=software`:
4K60 AV1 before: SIGABRT on the second frame, every run
after: exit 0, 0 panics, 35-39 fps, 1204 frames, decode_failed=0,
and 13 decode errors recovered from across 17 backlog
flushes — the exact condition that used to abort, survived
thirteen times in one session
1080p AV1 after: 40 fps, decode p50 2.2 ms (2.1 ms before the change)
What this does NOT buy: rav1d has other `unwrap()`s, and because its whole
public surface is dav1d's `extern "C"` ABI — every internal `rav1d_*` entry
point is `pub(crate)` — no in-process guard can turn one of them into anything
but an abort. This removes the one we hit and can prove; it does not make the
CPU rung panic-proof, and the evidence table says so.
Reported upstream with a self-contained reproducer: the in-tree
`test-25fps.ivf.av1` vector with one temporal unit dropped aborts rav1d at
`n_fc = 1`, survives at `n_fc = 2`, and libdav1d decodes it with 145 error
reports and no crash.
|
||
|
|
0ee690f8bc |
fix(client/hevc): the DPB we demanded was the level's ceiling, not the stream's need
ci / bun-nix (pull_request) Successful in 34s
ci / web (pull_request) Successful in 1m12s
ci / docs-site (pull_request) Successful in 1m12s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m18s
ci / rust-arm64 (pull_request) Successful in 2m30s
android / android (pull_request) Successful in 3m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m21s
ci / rust (pull_request) Successful in 5m42s
A punktfunk client streaming HEVC from .21 (RTX 5070 Ti) refused every access
unit with "stream needs 17 DPB slots, device caps at 16", flushed, waited for an
IRAP, got a fresh IDR that needed 17 too, exhausted the decode ladder and
reconnected with HEVC excluded. On a build with no software HEVC decoder — there
is no permissively licensed one — that is not a slower path, it is losing the
codec.
The host was blameless. Reading the SPS it actually emitted: general_level_idc
153 (L5.1 High, which NVENC autoselects at hevcConfig.level = 0 because a
130 Mbps target does not fit L5.0's 100 Mbps ceiling) and
sps_max_dec_pic_buffering_minus1 = 5 — six pictures, RFI_DPB references plus the
current one. Six, at every resolution. That is already the minimum the encoder
can honestly declare, and the only host-side lever, the level, cannot be lowered
without signalling a bitrate the stream exceeds. There was nothing to fix there.
dpb_limit was reading equation A-2 instead. A-2 is a CEILING on what an SPS may
signal — 7.4.3.2.1 constrains sps_max_dec_pic_buffering_minus1 to
0..=MaxDpbSize-1 — not a statement of what a stream needs, and it branches on
picture size against the LEVEL's MaxLumaPs. At 1080p the coded 1920x1088 =
2 088 960 luma samples fall under MaxLumaPs(L5.1) >> 2 = 2 228 224, taking the
first branch for min(4 * MaxDpbPicBuf, 16) = 16. max(A-2, buffering) then
reported 16 where the stream had asked for 6, the backends added one slot for the
picture in flight, and 17 is one more than NVIDIA's maxDpbSlots.
A resolution sweep on the box drew A-2's branch table exactly, and it is the two
commonest streaming resolutions that lost the codec:
720p 1280x720 = 921 600 branch 1 -> 16 frames, 17 slots 82 refusals, HEVC dropped
1080p 1920x1088 = 2 088 960 branch 1 -> 16 frames, 17 slots 41 refusals, HEVC dropped
1440p 2560x1440 = 3 686 400 branch 2 -> 12 frames, 13 slots clean
4K 3840x2176 = 8 355 840 else -> 6 frames, 7 slots clean, decode 1.9 ms
One host, one level, one six-picture requirement. Only which branch the picture
size landed in decided whether HEVC worked. That is also why this hid for so
long: 4K was the resolution it was exercised at, and 4K is the one size that
falls through to the honest answer. H.264 escaped for an unrelated reason — its
own level-derived ceiling happened to land at 13 for 1080p L5.0 and 5 for 4K
L5.2 — but it is the same shape of derivation and would fail the same way if
NVENC ever picked a higher level for a smaller picture.
So dpb_limit now returns the stream's own sps_max_dec_pic_buffering_minus1 + 1,
capped at 16. That is not a workaround, it is what the number means: it is
exactly the bound C.5.2.2's fullness clause bumps against, and A.4.1 bounds the
total RPS entries by the same value, so `buffering` pictures hold `buffering - 1`
references plus the current one with nothing left over.
The max() that produced the 16 was written to be generous to malformed streams —
"storing their pictures beats erroring the AU" — but it never did that either.
Dpb::needs_bumping (C.5.2.2) already keys on the signalled buffering, not on
max_num_pics, so a stream referencing more pictures than it declared was ALREADY
being bumped below its own declared depth before every store. The widened limit
bought no tolerance at all; all it ever did was over-allocate hardware surfaces,
by ten pictures per session at 1080p, and on NVIDIA take HEVC away entirely.
The fix moves 720p and 1080p onto the pool shape 4K has been running in the field
all along (7 slots, 6 references), so it is not a new operating point — it is the
one already proven. max_active_references drops from 15 to 6, still above the 5
an RFI_DPB stream can name. The per-AU level gate in pf-vkdecode reads
plan.picture.level_idc directly, so dropping A-2 out of NegotiationInfo costs no
sensitivity to a mid-stream level change.
Two regression tests pin the arithmetic from both ends, because either end
drifting back reproduces this:
- h265: the field SPS synthesized byte for byte on the fields that matter must
plan 6 frames / 7 slots, all four resolutions must agree because the stream
does, and every depth the envelope gate admits must leave room for the picture
in flight. The one honest residue is pinned too and deliberately left
refusing: A.4 does let a conforming stream declare a full 16-picture DPB, and
17 slots genuinely do not fit 16, so that stream is still refused rather than
decoded with too few slots and silently corrupted references.
- pf-encode: RFI_DPB + 2 <= 16, guarding the producer end. RFI is a real
latency win and this does not cap it at today's value — there are nine slots
of headroom — it just stops it being raised past the point where clients can
no longer decode us at all.
|
||
|
|
6348334eff |
docs(client/video): the evidence table stops saying AV1 never decoded
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m15s
ci / docs-site (pull_request) Successful in 1m19s
ci / rust-arm64 (pull_request) Successful in 1m40s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
android / android (pull_request) Successful in 3m33s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m4s
ci / rust (pull_request) Successful in 5m32s
Two of its notes became false the moment the host stopped truncating AV1. native D3D11VA / AV1 said "NEVER decoded a frame on any hardware". It has now decoded 4K60 on an RTX 3500 Ada — and the same run is why the note matters: its warn line named the rung as unproven moments before it failed 72 access units running with "reference picture N holds no DPB slot". That was the host shipping half of every frame, not the rung, so the M7 wiring was right all along. It stays UNVERIFIED regardless. `verified` gates `native_rung_admitted` — whether `auto` may pick this rung ahead of Vulkan Video — and one 25-second session with no frame-hash parity and no soak does not buy that. Promoting it wants a deliberate gpu_parity-style run. The note now says what is true instead of what is convenient. software / AV1 said rav1d had "CPU unit tests only". rav1d has now run on glass: 1080p AV1 decodes, and 4K ABORTS THE PROCESS. It takes an internal error path and panics inside its own on_error (rav1d 1.1.0 decode.rs:4997, unwrap on a None frame header); the panic crosses the extern "C" boundary in dav1d_send_data, so it is panic_cannot_unwind and no rung demotion or NoSoftwareRung refusal can catch it. libdav1d decodes the same 4K stream 715/715, so this is rav1d's own defect and is recorded where the next person to reach that rung will see it. |
||
|
|
ebc2f1cf92 |
feat(host/audio): the minted microphone returns to tier-0 — pitch-true
ci / web (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 2m18s
android / android (pull_request) Successful in 3m27s
ci / rust-arm64 (pull_request) Successful in 4m24s
ci / rust (pull_request) Canceled after 7m47s
The revert un-reverts, on measurement: with the per-direction stamp sets (render = the pad-proven PCM16-device/float-mix stereo split, capture = device-format only), micpitch reads 440 Hz in as 440 Hz out at exact peak. The octave-low voice was the driver DEFAULT endpoints disagreeing (stereo render vs mono capture), never a raw-crossing design. The user called the wrong verdict — the pad program 4ch success was the counter-evidence that reopened the case. |
||
|
|
c16e07d746 |
fix(encode/nvenc): AV1 stops shipping half a frame
Every 4K AV1 frame this host encoded reached the wire truncated to its first tile, and had since AV1 was wired up. Measured on .21 (RTX 5070 Ti, 4K60, split AUTO): each access unit carried a frame header declaring two tile rows and a single Tile Group OBU with tg_start = tg_end = 0, so libdav1d rejected 835 of 836 AUs with "Error parsing frame header". NVIDIA's hardware decoder accepts the truncated stream, which is why native Vulkan Video looked healthy at 60 fps while both conformant software decoders — rav1d in-tree and libdav1d out-of-tree — refused every frame and clients fell to a black screen. The two halves of sub-frame readback are armed by different conditions. build_init_params arms the WRITER (enableSubFrameWrite + reportSliceOffsets) from subframe_on alone; the chunked READER additionally requires slices >= 2, and resolve_slices returns 1 for AV1 unconditionally — before the PUNKTFUNK_NVENC_SLICES override is even read, because AV1 partitions via tiles rather than slices. So an AV1 session asked the driver to publish its output tile by tile and then took only the first tile with one blocking lock_bitstream. resolve_split_subframe — the one arbitration point both direct-SDK backends already call — now disarms sub-frame for AV1 and returns split_mode untouched, so AV1 keeps every engine split encode gives it. Arming the reader instead is not a drop-in alternative: poll_chunk cuts at bitstreamSizeInBytes on the reasoning that "slices are contiguous Annex-B", which AV1's OBUs are not. With sub-frame disarmed and split still AUTO, the same session decodes 654/654 frames clean through libdav1d. The test that pinned this as correct (av1_untouched, "both features are legal together") is replaced by one that pins the disarm, and by one that checks the reader's gate against the writer's — the comparison nothing made. The Linux latch comment claiming the two "can't disagree" is corrected; that claim is what made this invisible. |
||
|
|
52cd42e911 |
fix(host/audio): capture endpoints get the device format only
Live bisect on a fresh endpoint: the mix/host format keys are RENDER-engine properties — stamped onto a capture endpoint they broke its shared-mode graph (IsFormatSupported reported 2ch/48k OK while Initialize failed 0x88890008 on a once-stamped fresh endpoint; unstamped it opened fine, S3). The capture now gets ONLY the device-format key — the knob mmsys.cpl itself writes — declaring the stereo the pins actually accept. |
||
|
|
5c4969fd6b |
fix(client/pads): the gyro cut-off asked about the session, not the pad
Supersedes the check |
||
|
|
8c274d6256 |
fix(host/devtest): the probe asks stereo — its mono ask WAS the unopenable endpoint
Measured resolution of the 0x88890008 mystery: IsFormatSupported said the capture accepts 2ch/48k shared while Initialize kept failing — because the probe itself had switched to a MONO ask for frequency counting, and this stack does not bridge channel counts on capture even under autoconvert. Every unopenable-endpoint verdict after that switch was the instrument, not the endpoint. Stereo ask restored; crossings counted on channel 0. |
||
|
|
16e506f943 |
feat(host/devtest): audio-probe micpins — the driver-capability map
Exclusive+shared IsFormatSupported across {1,2}ch x {16,32}bit x
{44.1,48,96}kHz on both minted mic pins. Interrogates the DRIVER,
bypassing every endpoint-store stamping question: what the pins truly
accept decides whether the mic leg has any coherent configuration, and
whether an exclusive-mode mono open is an escape hatch. (The pad program
made its own breakthrough with exactly this instrument on the sibling
SSS driver.)
|
||
|
|
2eed9823e5 |
fix(host/audio): the mic pair gets the pad-proven coherent stereo stamp set
The user challenged the format-locked-pins verdict, and the pad program is the counter-evidence: it hit the SAME 0x88890008 unopenable-endpoint signature and cured it with a COHERENT stamp set, after which the same driver family served 4ch happily. This branch previous attempts were contaminated twice over — a float device-format (the pad bisect proved the split must be PCM16 device / float mix+host) and no AudioEndpointBuilder restart (Restart-Service Audiosrv never touches its dependency, so endpoint configs were never rebuilt). Both mic endpoints now get one identical coherent stereo set; the octave-low hypothesis shifts from "raw crossing by design" to "the two endpoint stores disagreed (stereo render default vs mono capture default)". |
||
|
|
cc53b3d6b0 |
fix(host/audio): unwire the minted microphone — the driver mic path is unusable
Final pitch-probe verdict on the SSM driver pair: the render pin is stereo-only, the capture pin mono-only (stamping either differently makes the endpoint unopenable), and the crossing between them is a RAW byte pass — so voice fed through the render endpoint reads back an octave low and no format stamp can fix it. S3 peak-based PASS = false pass; per the design doc revert clause the mic falls back to the name ladder (a virtual cable), pending the user re-decision. The SPEAKERS substrate keeps tier-0 (no driver crossing — a plain engine loopback tap, measured clean). minted_ids() publishes speakers only; the mic endpoints stay minted and recorded (provisioned()) for the micpitch probe and a possible future non-render transport, and their format stamps now pin each side to its pin one true format — healing the endpoints this branch earlier mis-stamped. |
||
|
|
5922cbe325 |
fix(host/audio): the minted mic pair declares stereo on BOTH sides
Second measurement round: the driver render pin is STEREO-ONLY — the mono render stamp turned the endpoint unopenable (0x88890008 on every open, the incoherent-stamp signature the pad program documented). Since the crossing is raw, the coherent choice inverts: the CAPTURE side now declares the stereo float stream that actually crosses (fixing the octave-low voice), and the render has its stereo float default stamped explicitly — pinning the pair AND healing any endpoint a previous build left mono-stamped. |
||
|
|
ed98814145 |
fix(host/audio): the minted microphone renders MONO — voice was an octave low
Measured with the new pitch probe: 440 Hz into the minted mic render came back as 220 Hz off its capture side. The driver forwards the render stream RAW into its mono capture, so a stereo-declared render (the driver-default we inherited) turns every stereo frame into two mono samples — half speed, octave down, exactly the field report. The mic render now gets a coherent MONO 48 kHz format set stamped alongside its name (PCM16 device format + float mix/host formats), making the engine downmix before the driver crossing. The mic pump keeps pushing stereo; shared-mode autoconvert handles the rest. |