fa2bcd9dbb82dcf602cf7479f837310cf1c2fb87
208
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cabd011f1d |
Merge pull request 'The 272 ms audio buffer was legal: the PipeWire callback filled the buffer ceiling, not the graph's request' (#106) from fix/pw-playback-requested into main
apple / swift (push) Successful in 1m34s
ci / web (push) Successful in 1m3s
ci / bun-nix (push) Successful in 17s
ci / docs-site (push) Successful in 1m15s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m37s
ci / rust-arm64 (push) Successful in 3m1s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 27s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
android / android (push) Successful in 5m6s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m43s
apple / screenshots (push) Successful in 6m5s
deb / build-publish (push) Successful in 4m8s
deb / build-publish-host (push) Successful in 3m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m9s
deb / build-publish-client-arm64 (push) Successful in 4m36s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m11s
ci / rust (push) Canceled after 2m31s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
arch / build-publish (push) Successful in 8m34s
flatpak / build-publish (push) Successful in 18m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 22m4s
Reviewed-on: #106 |
||
|
|
be86cfcdc0 |
fix(client/audio): the PipeWire callback stops filling the buffer ceiling every cycle
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m12s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m0s
ci / docs-site (pull_request) Successful in 1m8s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m18s
ci / rust-arm64 (pull_request) Successful in 1m40s
android / android (pull_request) Successful in 5m24s
ci / rust (pull_request) Successful in 12m51s
The playback process callback sized its writes from the mapped buffer's capacity — PipeWire's quantum-limit, 8192 frames ≈ 170 ms — instead of the graph's per-cycle ask (pw_buffer.requested). Every cycle therefore queued up to 170 ms of PCM downstream of the ring, and, worse, taught JitterPolicy that the device drains 170 ms per callback: the underrun floor (want + one frame) rose above any depth the A/V sync loop may request, so sync measured audio ~280 ms late and was forbidden — by its own continuity rule — from draining it. The first on-glass run of the latency overhaul showed exactly that: audio buffer 272 ms, a/v +284 ms, stable. Honor requested (capacity remains both the ceiling and the fallback for requested == 0), and log requested-vs-capacity once per stream in the shape of the host's per-capture-open quantum line, so the next on-glass report can say which one is sizing the writes. Needs libpipewire >= 0.3.49 (2022-03) for the requested field; every ship target clears that. Verified on .21: cargo clippy -p pf-client-core --all-targets -D warnings clean, 167 tests pass, fmt clean. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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. |
||
|
|
5c4969fd6b |
fix(client/pads): the gyro cut-off asked about the session, not the pad
Supersedes the check |
||
|
|
c34e1412fb |
fix(client): a decoder pin with a stray space was silently ignored
ci / bun-nix (pull_request) Successful in 20s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m7s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m32s
ci / docs-site (pull_request) Successful in 1m44s
ci / rust-arm64 (pull_request) Successful in 2m23s
android / android (pull_request) Successful in 3m9s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m7s
ci / rust (pull_request) Successful in 6m0s
Found on glass, and it cost a whole session to find. PUNKTFUNK_DECODER was read
untrimmed, so "native-vulkan " — ONE trailing space — matched no arm of
native_vulkan_gate, fell through to `auto`, and on an Intel box `auto` takes
d3d11va first. The operator's pin never ran and NOTHING said so. Read against a
log, that is indistinguishable from the rung being refused for a hardware
reason, which is precisely the ambiguity the rest of this module's logging was
just rewritten to remove.
The space is not exotic. A Windows .cmd produces it for free: `echo x>> file`
keeps the space before the redirect, so every line written that way carries one.
PUNKTFUNK_VK_ADAPTER already trimmed; this did not, and the inconsistency is what
made it invisible — the GPU override obeyed while the decoder override did not.
The rule now lives in one pure function, resolve_decoder_pref, called by BOTH
readers. decode_pinned_to_software had the identical untrimmed expression, and
its own doc comment says a second reading of the same two inputs is a second
place for them to drift — fixing one and not the other would have proved it
right. Whitespace-only counts as ABSENT rather than as a pin to "", because an
exported-but-empty variable means "no override" and "" is a value the gate
happens to accept.
Tested as a pure rule (no process environment), including the end-to-end leg
that matters: the trimmed pin reaches native_vulkan_gate and is admitted. Like
the create-array tests in
|
||
|
|
77797a9e20 |
feat(client/pads): stop streaming gyro into a session that cannot receive it
G8 of the gyro program, SDL-client half. The `Welcome` has always carried the backend the host actually RESOLVED, which is not necessarily the one the client asked for — Auto lands on Xbox 360 for anything not Sony/Valve/Xbox, and a Switch Pro on a Windows host folds to X360 too. No client read the field. So a player with an 8BitDo, or a Switch Pro on Windows, got a controller whose gyro did nothing, with nothing anywhere saying why: the client shipped ~250 Hz of Motion datagrams and the host parsed and discarded every one. `GamepadPref::has_motion()` answers whether a backend has a motion plane at all. The SDL client checks it on the first gyro sample: it logs one line naming the resolved backend and pointing at the fix (pick a DualSense-class controller type), then stops sending. Once per slot, not per sample — this path runs at the pad's sensor rate. `Auto` deliberately answers true. It means "unknown" — an old host that omitted the echo, which may well have resolved a DualSense — and suppressing motion on unknown would silently break working gyro, a worse failure than sending datagrams nobody reads. The predicate is an exhaustive match so a new backend has to state its answer rather than inherit one, and a table test pins both halves: a false negative kills working motion, a false positive keeps the void open, and both are silent. Owed: the plan wants this surfaced as a one-line UI hint, not just a log line. Apple already stores `resolvedGamepad` and Android needs the plumb; neither is done here, and both want their own gate. Gate (Linux CI image): fmt, build, clippy --all-targets -D warnings, and the test suites — green, with the new capability test observed running. |
||
|
|
4834c2ee51 |
fix(host/pads): DualShock 4 gyro ran 40× fast, and no pad ever stopped turning
Phase 1 of the gyro program (design/gyro-program.md, G1-G5) — the five correctness fixes under it. Gyro aim integrates angular velocity over time, so each of these is not a cosmetic wrongness: a wrong scale is every rotation being the wrong size, a wrong clock is every rotation being integrated against a fictional dt, and a stale sample is rotation that never happened. G1 — the DualShock 4 calibration blob. A Sony pad does not assume a motion scale, it reads one out of a fixed calibration feature report. Ours declared 0.5 LSB per °/s and 8192 LSB/g while the wire delivers 20 and 10000, so every DS4-type session decoded gyro 40× too fast and acceleration 1.22× hot — since the backend shipped. The blob now states the wire's own units (the DualSense blob's numbers, deliberately: both pads consume the identical wire sample). Its interleaved per-axis order is NOT a bug and stays: the virtual pad declares BUS_USB, where interleaved is the correct layout; grouped is Bluetooth's. The same blob lives a second time in the UMDF driver, which is a separate WDK workspace that cannot depend on pf-inject — one wrong table in two files, where fixing one reads as fixing it. Both are fixed, and the DS4 feature reports now live in dualshock4_proto beside the DualSense's rather than in the Linux backend, so there is one canonical copy to point at. Field hosts keep the old blob until they update the host package. G2 — the gate that would have caught it. Nothing pinned any backend's declaration against the wire, so tests/motion_contract.rs now applies the CONSUMER's arithmetic (the kernel's, and SDL's, which differ) to each backend and asserts the result lands back on the wire constants — for the DualSense and DS4 blobs, and for the Deck and Switch Pro rescales. It also parses the driver's Rust source and re-derives the units from THAT, so the two copies cannot drift. Verified non-vacuous both ways: re-introducing the old blob fails with "declares a fractional 32/64 LSB per °/s", and reverting only the driver's copy fails with "the UMDF driver's DS4_FEATURE_CALIBRATION has drifted from pf-inject's". The wire units themselves move to punktfunk_core::input::gamepad, referenced by the client's capture scale, the Deck/Switch rescales, and the probe — whose at-rest vector said 16384 (a driver's number, not the wire's) and now says 1 g. G3 — real sensor clocks. The DualSense advanced its sensor timestamp by +1 raw unit per report (0.33 µs — a frozen clock) and the DS4 by a flat +188 (~1 ms) regardless of the real 4-8 ms cadence. Anything integrating rate × dt off that field got nonsense. All four backends now stamp elapsed monotonic time in their own units via a shared SensorClock, anchored to the pad's first report so an irregular publish loop cannot make it drift, and truncated to the field width — which reproduces the wrap real hardware does. G4 — motion is level-triggered and had no watchdog. merge_frame preserves the last sample and the heartbeat re-emits it, so a feed that stops leaves the pad rotating forever — and with G3's honest clock, at a dt that keeps growing. Rumble and the pen plane each have an idle timeout; motion now has one too, at 100 ms. Angular velocity only: acceleration is kept, because gravity is legitimately persistent and blanking it reads as free-fall. The SDL client parks its gyro at zero when a slot closes, which is the case we can flush rather than wait out. (The Apple half of this rides in PR #88.) G5 — a pad returning inside the 300 ms replug grace keeps the same device and skips the create path, so a different controller inherits the previous one's touch contact and rotation — and a pad with no gyro never sends a sample to correct it. sweep() now reports re-claims separately from drops, and the manager clears the rich plane on one. Rich fields only: rumble and hidout dedup deliberately survive a removal. Gates (Linux, CI image): fmt, build, clippy --all-targets -D warnings over pf-inject/punktfunk-core/punktfunk-probe/pf-client-core, and the test suites — 110 pf-inject unit + 6 contract + 29 pf-client-core gamepad, all green. Not yet verified on glass; the on-glass sign/scale session is G16. |
||
|
|
413a3e3e76 |
feat(client): say which GPU can do Vulkan Video, and why not when it can't
ci / bun-nix (pull_request) Successful in 29s
ci / rust-arm64 (pull_request) Successful in 1m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m5s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 3m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m1s
ci / rust (pull_request) Successful in 4m27s
ci / docs-site (pull_request) Failing after 16m24s
ci / web (pull_request) Failing after 16m25s
Field report from an Intel Arc + NVIDIA laptop: pinning the Vulkan rung on the Arc iGPU silently produced D3D11VA, and there was no way to tell whether the build had tried at all. That ambiguity was ours, in three places. The "unavailable" log printed three of the FIVE conjuncts that gate Vulkan Video. A device with 1.3, the features and a decode queue family — but no codec extension — logged dev_is_13=true features_ok=true decode_family=true next to the word "unavailable" and named nothing actionable. It now prints all five, plus which base extensions are missing, which codec extensions are present, the decode family's own advertised codec operations, and the device name and vendor. It also no longer says "VAAPI/software" on Windows, where the rung below is D3D11VA. The native-vulkan PIN refusal logged `video_decode` alone. On a device that decodes something but not THIS codec, that reads as a contradiction: refused, yet video_decode=true. It now carries the caps mask and the codec bit that was wanted, so "your GPU can't" is distinguishable from "we asked for the wrong thing" — only the second is our bug. And `--probe-decode` is new: per-adapter Vulkan Video capability with no session, no surface and no logical device. For each GPU it answers usable yes/no, the driver's own decode ops, the extensions, and — when the answer is no — which conjunct failed, in words. Separate from --list-adapters, which the desktop shells parse line-by-line for their GPU picker and which therefore keeps printing bare names. The listing is ordered like pick_device (discrete first) and marks entry 0 as the default presenter, because that ordering is very likely the reporter's actual answer: pick_device ranks DISCRETE_GPU above INTEGRATED_GPU, Vulkan Video decodes on the PRESENTER's device by design (that is what makes it zero-copy), and PUNKTFUNK_DECODER does not move the presenter. So on a hybrid laptop, pinning the decoder while the dGPU presents probes the wrong GPU entirely — PUNKTFUNK_VK_DEVICE=<index> is the knob that moves it, and the index printed is that value. To keep the probe honest, VIDEO_BASE and VIDEO_CODECS moved to module scope and the five-way AND became video_decode_gate(), called by both the probe and device creation. A probe holding its own copy of the rule is one that eventually reports a capability the session then refuses — which reads to everyone as a decoder bug rather than a probe bug. Gates: fmt clean; clippy -D warnings over punktfunk-client-session and pf-presenter. The Linux container was unavailable (the host's disk filled and took the docker daemon with it), so this ran on the macOS host target only — the container leg is owed, and CI covers it on the PR. |
||
|
|
bbbcf321e5 |
Merge origin/main into worktree-native-decode-m0
ci / web (pull_request) Successful in 1m19s
apple / swift (pull_request) Successful in 1m32s
ci / docs-site (pull_request) Successful in 1m23s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 25s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m8s
android / android (pull_request) Successful in 3m31s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m23s
ci / rust-arm64 (pull_request) Successful in 5m36s
nix / flake (pull_request) Failing after 11m59s
ci / rust (pull_request) Successful in 14m27s
main moved 93 commits while this branch ran. Two conflicts, both where main's new
work sat next to M10's excision:
packaging/flatpak/io.unom.Punktfunk.yml — main added the vendored gamescope WSI
layer (the only route to HDR on a Deck) and, before it, a vulkan-headers module.
Took both: this branch predates them and deletes neither. But the headers module's
stated consumer was pf-ffvk's bindgen over FFmpeg's hwcontext_vulkan.h, and M10
deleted pf-ffvk — so it now reads as dead weight to the next person. It is not:
the WSI layer IS a Vulkan layer, compiles against those headers, and builds after
it, so module order is the dependency. Rewrote the rationale to say so, including
why dropping it would be expensive to discover — flatpak.yml has no pull_request:
trigger, so a manifest break reaches main invisibly and a tag then ships no Linux
flatpak. Also recorded that the native decoder needs nothing from there: pf-vkdecode
reaches Vulkan through ash, which is pure Rust bindings, no bindgen, no C headers.
crates/pf-console-ui/src/screens/settings.rs — main restructured the gamepad
settings into TABS, which removed the per-row section headers; this branch had left
Some("Video") untouched from the merge base and added the pre-M10 decoder migration
next to it. Git could not tell those apart. Took main's structure (no header, its
deliberate change) with this branch's migration layered on: a stored `vulkan`,
`vaapi` or `d3d11va` names no preset in the tabbed list and would render as "—",
then silently rewrite the user's preference on the next save.
Gates on the merged tree, Linux container: fmt clean; cargo check --workspace
--all-targets clean; clippy --workspace --all-targets -D warnings clean; tests
green across pf-vkdecode (187), pf-client-core (163), pf-console-ui (58) and
punktfunk-host (447 of 448 — the one failure is the pre-existing
gamestream::stream::tests::sender_delivers_batches, a UDP-loopback EINTR under
qemu that fails identically on a pristine HEAD).
|
||
|
|
5c05246098 |
feat: M10 — FFmpeg is gone from the client
cargo tree -p punktfunk-client-session finds no ffmpeg. The host still does, which is the whole point: pf-encode keeps libavcodec unconditionally and no host workflow, packaging script or licence file was touched. Deleted: crates/pf-ffvk, video_vulkan.rs, video_vaapi.rs, video_libav.rs, the libavcodec half of video_d3d11.rs, the av_log machinery, ffmpeg::codec::Id as the decoder's vocabulary (the quic CODEC_* wire constants now serve, which is why the evidence table was keyed on them), DecodedImage::VkFrame and ::Dmabuf, the presenter's AVVkFrame lane, and the ffmpeg-fallback feature with everything behind it. DrmFrameGuard collapses from an enum to a newtype, which removes an unsafe impl Send. Roughly 25,000 lines. Then the CI, packaging, licensing and docs work the plan's §6 lists: the Windows workflows lose FFMPEG_DIR, PF_FFVK_VULKAN_INCLUDE and their PATH prepend; the MSIX loses its DLL wildcard; the client .deb stops emitting libav sonames on its own because depends come from dpkg-shlibdeps; arch, flatpak and nix drop the dependency; and the README's "FFmpeg 7 or 8" contract narrows to the host. Three defects reached users' machines in the first cut, and none was in the deletion itself. All three desktop Settings UIs offer vulkan, vaapi and d3d11va as stored decoder values, so those strings sit in shipped settings files today. Refusing them by name — which is the correct rule for a stale pin — would have bricked every upgraded client whose owner ever touched that dropdown. They now migrate onto the native rung for the same hardware family, at decoder construction AND at each dialog's lookup, because a legacy value that matches no preset displays as "Automatic" and silently rewrites the user's preference on the next save. M9's evidence filter was deleted on the argument that with no libavcodec twin below, barring an unproven rung removes hardware decode rather than moving down one rung. That is true on Windows and false on Linux for Intel and every unknown vendor id, where prefer_vulkan_first is false and the order is native-vaapi → native-vk: a rung that has decoded nothing anywhere sitting above one that is 250/250 on three drivers. Every Intel Linux desktop would have moved from libavcodec VAAPI, shipping for years, onto pf-vaadec by default — and a rung that constructs and then produces wrong pixels leaves only by the error-streak demotion, which this codebase already documents as not tripping on the B580's strobing. The filter is restored as a narrow, pure, testable rule: an unproven rung yields to a proven one, and to nothing else. Windows deliberately passes no rung below, because that vendor family is the one with a measured wrong-pixel report against Vulkan decode, and trading no evidence for evidence of corruption is the wrong direction. And the notices still said FFmpeg was bundled. The root file is what both desktop clients include_str! and what the MSIX ships, three lines under the new card saying no FFmpeg is bundled; Apple's Acknowledgements said it too, on iOS, tvOS and macOS. The generator now emits four per-client files scoped by transitive closure — 0 FFmpeg mentions in each, verified — while the root file keeps it for the host. That also ends the standing false attribution of ffmpeg-next, GTK4, windows-rs and the NVENC SDK to an iPhone. Windows has no reachable box, so it was compiled instead: a cross clippy at -D warnings on x86_64 and aarch64-pc-windows-msvc with the C toolchain stubbed so build scripts run without linking. That gate immediately caught an include_str! path one directory too deep, which nothing else could have. Gates: container clippy -D warnings, 160 tests, workspace check, both Windows targets clean, client ffmpeg count 0 and host 2. The four decode crates are untouched, so the hardware rungs' 250/250 stands. ⚠ Owed and unrun: no GPU has executed any of this milestone. M8's on-glass software check, M7's D3D11 and VAAPI AV1 hardware legs, and M9's field bake all still want hardware, and the bake window and criteria remain the user's. |
||
|
|
38554c1c6e |
feat(client): M9's code half — native first, FFmpeg behind an off-by-default feature
`ffmpeg-fallback` on pf-client-core, default off on the crate. With it off the libavcodec rungs are not compiled, pf-ffvk leaves the dependency graph, and no ladder or demotion arm names them; with it on each sits exactly where it sits today, directly below its native twin. That is the switch which makes M10 a deletion rather than a redesign. The bake window and the regression criteria are the user's, per the plan, and nothing here claims the M9 gate is met. The hard part was not the feature, it was honesty. Two of the four native rungs have never decoded a frame on any hardware — native VAAPI at all, and native D3D11VA's AV1 leg — and making those the default would assert evidence that does not exist. So admission is per rung and per codec: a pair with hardware evidence joins `auto` always; a pair without it joins only when nothing proven is left below it (a build with no FFmpeg twin, where the alternative is not a proven rung but the CPU) or when the user asks with PUNKTFUNK_NATIVE_FIRST=1. Pins bypass it, so a lab run can still reach any rung. The shipping default therefore changes in exactly three ways, all evidence-backed: AV1 `auto` takes native Vulkan (250/250 bit-identical on an RTX 5070 Ti), Windows H.264/H.265 `auto` takes native D3D11VA above its FFmpeg twin (parity on two GPUs plus a 30-minute soak), and a failing Vulkan rung on Windows demotes to native D3D11VA first. Everything unproven is byte-for-byte as it was. The evidence state is written where it cannot rot: a table in video.rs's module docs, the same facts in code as `native_evidence()`, a test asserting them in both feature states, and a per-session log line carrying the rung, the codec, whether hardware has verified that pair and the evidence string — at WARN when it has not. A support engineer reading a log can now tell proven from assumed without asking anyone. Termination needed a new guarantee. With the FFmpeg twins gone, two native rungs in opposite per-vendor orders could hand a session back and forth forever, so a rung once entered is never re-entered and the walk is monotone to software. The never-delivered fall-through still works: with the feature on it is unchanged, and with it off it is redundant, because the next candidate already IS the rung below. ⚠ ffmpeg-next remains a hard dependency of pf-client-core, deliberately. What is left off-feature is three type-level residues — the codec-id vocabulary, the AVVkFrame guard that is pf-presenter's public import, and a pixel-format in one signature — every one of them an M10 §6 line item. Deleting them here would mean deleting the presenter's FFmpeg lane, 55 call sites, in a milestone whose gates cannot run a GPU. No libavcodec decoder is opened in a default build. ⚠ video_d3d11.rs was gated item by item rather than wholesale, and nothing in this tree compiles it — it needs a Windows check before anyone trusts it. Gates: both feature states, container clippy -D warnings and 158/159 tests, workspace check. The four decode crates are untouched, so the hardware rungs' 250/250 stands. |
||
|
|
d5e23146c0 |
feat(client): M8 — the software rung is openh264 and rav1d, and swscale is gone
The ladder's last rung no longer runs FFmpeg. H.264 decodes through openh264, AV1 through rav1d, and HEVC is refused outright: no permissively licensed software HEVC decoder exists, so an HEVC session that exhausts its hardware rungs now tears down and re-dials advertising HEVC-less caps, and the host picks H.264. The plan calls that a first-class path; it is one. swscale is deleted, and with it the BT.601 default that its correction code existed to undo. Colour on the H.264 lane now comes from the same pf-bitstream planner every hardware rung submits from — openh264 reports no VUI at all — and AV1's comes per-picture from the sequence header. One colour source, one CSC: the old default is unrepresentable rather than merely fixed. Frames reach the presenter as three tightly-packed planes through the planar CSC pass, which had to be un-gated from the pyrowave feature and its device probe, since the last rung must exist on devices that failed that probe. rav1d rather than the dav1d crate, deliberately and against the plan's literal wording: dav1d-sys is system-deps-only, so it would add a system library and a .pc file to every client package — in the milestone family whose excision checklist exists to delete exactly that. rav1d is the same decoder, same licence, statically linked. The cost is honest: no-asm builds on both decoders, and software throughput is still unmeasured. The colour test is the milestone's exit criterion, so it is built to fail. Three fixtures, and a mutation check: hardcoding the swscale default turns the red bar to [255,24,0], and swapping Cb/Cr turns red to blue — a silent error no metadata assertion could catch. Review then disproved the range half of it numerically: with eight saturated bars, decoding the full-range fixture with the wrong range gives max error ZERO, because a mismatch only pushes values outside [0,1] where the shader clamps. A mid-tone was added; the wrong range now costs 11, well past the tolerance. The exit criterion I set was satisfiable by a test that proved nothing. Two blocking defects, both emergent rather than local. Software AV1 on a 10-bit stream never reached its typed refusal: rav1d is built 8-bit-only and returns ENOPROTOOPT, which the send loop turned into a generic error, so the pump's typed downcast missed and every AU failed identically — a permanent freeze on precisely the shipping case, since AV1 is advertised only where hardware AV1 exists and hardware AV1 plus HDR is Main 10. The shape is now read from the sequence header before any byte reaches the decoder, exactly as the H.264 leg reads the active SPS. And the new Reconnecting phase was the first state that is not streaming, not connecting, and still holding a live stream — which opened all three guards that had made a second launch impossible. Pressing A assigned over `stream` where every other site shuts down first, and StreamState has no Drop, so the old pump was detached: a second live session still submitting to a Vulkan device that gets destroyed underneath it. Nothing about the reconnect was wrong in isolation; the defect lived between a new state and three guards nobody re-examined. Start is now defensive and the retry raises the connecting modal, so the UI matches the state and B can cancel. Also closed: retry_caps was computed, tested and never applied, so a shape refusal could end a session reporting no codec available while a working retry existed; the retry inherited force_software sticky-true, landing an HEVC→H.264 fallback on software H.264 with working hardware H.264; it re-dialled with a stale mode; the CPU present arm had no survivable-failure handling where the pyrowave arm — same pass — has it; HEVC is no longer advertised when the decoder is pinned to software; and the software rung now feeds the recovery-point SEI it already had in hand to the re-anchor gate. ⚠ Two host-side gaps found while tracing, neither in scope here: Hello::launch is NOT idempotent (gog:/custom: targets spawn a second copy on a retry; the field is kept verbatim because dropping it orphans the gamescope display whose reuse key includes the command), and a reconnected session can never adopt a game predating its own launch stamp, so it has no game-exit detection. ⚠ OWED: the on-glass software run. ~200 lines of new Vulkan on a path that only runs because the GPU already failed, and no driver has seen it. The review's minimum check is sync validation enabled, a non-multiple-of-16 mode, a mid-session resize and demotion, and both colour matrices. Gates: container clippy -D warnings over four crates, 236 tests, workspace check. pf-vkdecode and pf-bitstream are byte-for-byte untouched, so the hardware rungs' 250/250 stands. |
||
|
|
a20cd44ed4 |
feat(client): native VAAPI AV1 — the third rung, and two failure-path defects
The libva AV1 layouts, the AuPlan conversion and the Linux rung's AV1 arm, completing AV1 across all three hardware backends. Pin-only. Layouts measured, not transcribed: the committed probe grew the AV1 structures and every size and offset it printed against libva 2.23.0 is a compile-time assertion. Three that a hand-count gets wrong — the picture buffer is align 8 because anchor_frames_list is a pointer, inserting seven bytes of padding; seg_info and film_grain_info carry their own padding tails inside the parent; and THREE of AV1's six bit-field unions are narrower than a word (one uint8_t, two uint16_t), so a u32 packer over any of them writes through its neighbour. This is the fifth way this program has had to spell "which pictures does this frame use", and it is unlike the other four: ref_frame_map is indexed by SLOT and holds actual VASurfaceIDs rather than indices into anything, ref_frame_idx is indexed by NAME and holds slots taken from the header — not from the plan's refs, where a lost reference leaves a hole and a hole is not a slot — global motion is picture-level, and there is no per-reference size field at all. Established from va_dec_av1.h and libavcodec's vaapi_av1.c, and stated in the module docs so the next reader does not re-derive it. Review verified the whole happy path — every layout assertion re-measured, every packer width and bit position, the reference convention, the num_elements buffer shape — and found both defects on FAILURE paths, neither reachable on the vendored vector. A conversion refusal permanently desynced the ledger. The mutation block sat after the tile walk, so any tile-shape refusal left the planner holding a picture with no ledger slot — and the resulting UnresolvedReference fires before that block too, so it never repaired. Every later access unit hard-errored until a shown key frame: one lost packet costing a GOP. The arm's own doc already warned that skipping conversion would desynchronise the slot map; the refusal door did exactly what the skip door was written to avoid. The block is hoisted, and a tile-shape refusal on an already-damaged plan is now concealed rather than refused. Fixing that exposed a sharper edge: the conversion can release a slot and reassign it to the refused picture in one call, so the binding would still hold the PREVIOUS picture's surface — a wrong reference rather than a missing one, which nothing downstream could notice. The caller now clears the binding unconditionally on the refusal path. And a damaged frame's surface was never written yet was bound as a reference and left in pending, so a later clean show_existing_frame would claim it with damaged = false and ship uninitialised GPU memory to the presenter — on several drivers another client's framebuffer. The justification quoted half of va_dec_av1.h; its next sentence gives the remedy, which is to point the problematic index at an alternative buffer. Damaged frames now submit as they do on the other two arms, with live surfaces substituted for invalid entries and reported as a bitmask — preferring a reference that really decoded over the decode target, and keeping libavcodec's deliberate all-invalid map on a shown key frame. Film grain is refused rather than decoded wrong: libva wants two surfaces, one ungrained for prediction and one grained for output, and libavcodec allocates a second frame for exactly that. The gate now sits after the mutation block so a grained frame costs itself rather than the GOP, and stays per-AU rather than per-sequence because a stream that merely DECLARES the tool decodes here perfectly. ⚠ Residual, flagged not fixed: a picture decoded from substituted references can still be shown by a later show_existing_frame. It is decoded memory now rather than uninitialised, and it is what the H.264/H.265 arms do, but tracking "this was concealed" through to display needs new session state. Gates: macOS fmt/clippy/125 tests/cargo-doc, container clippy -D warnings over seven crates and 548 tests, workspace check. pf-bitstream's diff is comment-only — verified — so the Vulkan rung's 250/250 stands untouched. Nothing here has decoded a frame: no VAAPI hardware is reachable. |
||
|
|
ef40890c80 |
feat(client): native D3D11VA AV1 — wired, and four defects it exposed
The AV1 arm of the native D3D11VA rung, parity-required because today's FFmpeg d3d11va rung already decodes AV1 Profile 0 and the excision must not silently drop it. Pin-only, as that rung is today. decode() walks the temporal unit frame by frame; submit() splits into decode_into and present, because AV1 decodes frames that are never shown. The proven H.264/H.265 body is byte-for-byte unchanged — review diffed it against HEAD mechanically and found only a rename plus one refusal arm — and the VideoProcessorBlt hand-off is untouched. That mattered more than anything else here: those two codecs are hardware-proven, .173 is powered off, and no gate that runs could have caught a regression in them. Every descriptor value comes from libavcodec's dxva2_av1.c read verbatim, not from symmetry with the other codecs: three buffers and no qmatrix (AV1 transmits none), NumMBsInBuffer zero on all three, ConfigBitstreamRaw 1, surface alignment 128, pool +8, and the session sized from the SEQUENCE header's max frame size — sizing from the frame would rebuild the decoder and drop every reference the first time a stream legally resized downward. Two places where following the H.264/HEVC pattern would have been wrong. libav pads the bitstream buffer and grows only its descriptor's DataSize, never a tile's, because a tile's size is exact — charging padding to the last record is corruption, not filler. And the committed tile records were one per tile GROUP spanning the whole OBU, header and frame header included, where libav emits one per TILE addressing the payload past its tile_size_minus_1; the vendored vector is single-tile, so the old tests passed either way. Review then found four more defects in the already-committed conversion, each confirmed against libavcodec AND Chromium's D3D11 AV1 accelerator: Tile widths and heights were the coded minus-1 where the field is a superblock COUNT — every tile declared one superblock short, on every frame, with a comment asserting the opposite of the truth. StatusReportFeedbackNumber must be zero for AV1. Both reference implementations disable it specifically for this codec — libav's note reads "breaks decoding on some drivers (tested on NVIDIA 457.09)", Chromium's "it crashes :|" — while both set it for H.264 and HEVC, which is why this rung's proven codecs never showed it. It would likely have presented as a hang or a rejected submission rather than bad pixels, sending the next session after the tile records instead. frame_refs[].Index is an index INTO RefFrameMapTextureIndex, not a surface index; the neighbouring line already filled that map correctly. Measured: 1636 reference entries on the vendored vector where the two differ. qm_y/u/v need the 0xFF "no matrix" sentinel — 0 is a valid matrix index, and 274 of 274 frames transmit no quantiser matrix, so every one was being dequantized against matrix 0. Also closed: the slot leak the Vulkan rung had already found and documented (a frame refreshing no slot is never reported removed, so nine of them exhaust the ledger); a tile-grid check that could not fire, replaced with libav's own cols*rows guard; per-reference sizes now taken from the reference's own header via RefState rather than the current frame's; and the render size clamped against the decoded picture in both rungs, since AV1 permits a render size larger than the frame. The parity leg was rewired through the real decode path — it previously called the internals directly, so its hidden-frame assertion described the harness's own counter rather than production withholding anything. Gates: macOS fmt/clippy/383 tests, container clippy -D warnings over four crates and 499 tests, and on Windows .133 (.173 is powered off) clean checks plus 97 pf-dxvadec tests. All 8 Vulkan gpu_parity legs re-verified bit-exact on the RTX 5070 Ti after the shared-code change. No AV1 frame has been decoded through this rung anywhere: it needs .173 back. |
||
|
|
a404830456 |
feat(client): wire AV1 into the native Vulkan rung, pin-only
The third codec arm in video_vk_native, AV1 admitted to native_codec and to native_vulkan_gate by pin only. It stays out of `auto` on the same rule M5's D3D11VA rung follows: `auto` admission is earned with hardware evidence, and this has decoded nothing on a device. is_integrity_warning_av1 did not exist, so the client could not have concealed AV1 damage at all. Added, exhaustive, no wildcard: all three AV1 warnings really are damage, because AV1 has no spec-legal-but-noisy signal to mis-classify — no reorder envelope to announce, no MMCO to rebase — and the exhaustive match is what stops a future variant defaulting to clean. The blocking defect review found was two safety mechanisms cancelling each other. After a failure the decoder skipped to the next key frame answering Ok(None), and because AV1's planner has no flush its store kept planning cleanly, so those AUs carried no warnings and the client read them as proof the rung works — clearing the demotion streak and resetting its clock on every one. The streak could then never reach the threshold, which made the never-delivered fall-through to FFmpeg-Vulkan unreachable, which is the documented backstop for exactly three things: a level above maxLevelIdc, a sequence header disagreeing with the Welcome, and film grain. Film grain is the probe's own admitted assumption, so a grain stream would have frozen the screen for the session while DecodeHealth reported run 0 — recovered. AV1 now answers the wait with an error, as H.264 and H.265 already do through AwaitingIdr, so all three codecs are indistinguishable to the demotion machinery. That matters more than the extra precision of a third state: only the H.26x paths have hardware evidence, and they are proven WITH that behaviour. The obvious form of that fix would have wedged the decoder. A key frame can sit behind a skipped frame inside the same temporal unit — the vendored vector has 24 two-frame units — so erroring out of the per-plan loop would never reach it and the wait would never end. Skips are therefore counted per frame and the error raised only when the whole unit was skipped, with the metadata-only unit staying a clean Ok(None). Also closed: a refused temporal unit left an already-decoded frame in the ready queue, which shipped on the next AU as a clean success — putting a picture from a refused AU on screen, clearing the streak again, and latching delivered so the fall-through was disabled for good. The error arm now drains and releases unshown. MAX_DELIVERABLE is derived rather than picked: HOLD_HEADROOM minus the pipeline's own hold, pinned to pf-vkdecode's constant so a hardcoded depth fails the build. At the previous 8 the queue plus the presenter's 4-7 stood against a headroom of 8, so it capped memory without preventing the exhaustion it named, and a frame waiting 8 AUs burned 16 of the 17 query slots — where a re-armed slot reads as Failed and becomes a fabricated driver-corruption verdict in the very counter the Ally X signal lives in. The trim now runs after this AU's frame is taken, or at the derived depth it would drop a two-output unit's first frame and invert display order inside one AU. Its justification was also wrong: the claim that a temporal unit may carry a show_existing_frame alongside a shown frame is disproved by this repo's own golden — 250 units, 250 shown, zero show_existing. The bound is kept as defence in depth against a non-conformant or multi-operating-point stream, and now says so. Gates: macOS fmt/clippy/392 tests, container clippy -D warnings over six crates, 851 tests, workspace check. No hardware: the rung is pin-only and has still never decoded a frame on a device. |
||
|
|
eecd04247f |
feat(client): AV1 is advertised on hardware, not on a decoder existing
The standing open item M7 was meant to close. `decodable_codecs` answered the AV1 bit from `ffmpeg::decoder::find(AV1)`, which says yes on every build that links libdav1d — a software decoder. So the client told the host "send me AV1" on machines that would then decode a 4K stream on the CPU, and codec negotiation happens once at Welcome, so there is nothing to fall back to afterwards. A promise the client cannot keep is worse than not making it. `av1_hardware_decodable` answers from device facts only: the presenter's Vulkan device listing DECODE_AV1 among its decode queue family's codec operations, or — on Windows — the D3D11 import path, which is the same gate the D3D11VA rung sits behind and that rung decodes AV1 Profile 0 today. VAAPI is deliberately not consulted: asking libva costs opening a display, and this is called too early and too often for that. The Vulkan bit covers the Mesa devices where VAAPI AV1 exists in practice, and a machine with VAAPI AV1 but no Vulkan AV1 loses the advertisement, not a working path. The test pins what the gate must not accept: a device that decodes H.264 and H.265 but lists no AV1 operation, and a device whose caps word claims AV1 while it has no decode queue at all. Gates: macOS fmt/clippy, container clippy -D warnings over six crates, 805 tests, workspace check. |
||
|
|
a6e51215fd |
feat(client): M6's rung is wired — libva, dlopen'd, no libavcodec
The native VAAPI decoder now runs end to end: pf-vaadec's plans go into libva's buffers, the surface comes back as DRM-PRIME dmabufs, and the presenter imports them exactly as it does the FFmpeg rung's. Pin-only — `PUNKTFUNK_DECODER=native-vaapi` — for the reason M5's D3D11VA rung was: `auto` admission is earned with hardware parity and a soak, and this rung has decoded nothing yet. libva is dlopen'd rather than linked, so the pf-lxcheck2 container compiles and clippies the whole thing without libva-dev, and a machine without a VAAPI runtime gets a clean refusal instead of a packaging dependency. The surface pool is not the slot map. `SlotMap::assign` hands out the lowest free slot, and a slot freed by an access unit's own removals is free by the time that unit's picture takes it — measured at 225 of the vendored vector's 250 access units. A surface bound by slot index would therefore decode, on nine frames in ten, into the surface still holding the picture on screen. So `plan_to_va` now takes the decode target as a parameter, bound by the caller at activation time the way pf-vkdecode binds a pool image, and a surface is free only when no live picture is bound to it, no output is owed for it, and no consumer holds it. Measured rather than transcribed, as everywhere else here: layout-probe.c grew the export descriptor (312 bytes, objects[4]/layers[4]), the buffer-type enumerators — VASliceParameterBufferType is 4 and VASliceDataBufferType is 5, not the 3 and 4 that counting off the header suggests — and the config, attribute and generic-value layouts. All pinned as compile-time assertions, which is how the 12-byte VAGenericValue in the first draft was caught: the C union holds a pointer, so it is 8-aligned and 16 bytes. The plane walk lives in pf-vaadec, pure and unit-tested on macOS, because it is the one structure the DRIVER writes and we read: SEPARATE_LAYERS returns NV12 as two layers, and taking layers[0] is the green screen this project has already paid for. It also refuses what it cannot express rather than guessing — a bogus object count, a plane naming an object that is not there, objects disagreeing on tiling. Own DecodedImage variant, same payload type. The physical hand-off is identical to the FFmpeg rung's, so the presenter keeps ONE arm and one demotion streak; the variant exists so the compiler asks which rung decoded wherever that matters. Both D3D11VA rungs share a variant and `1573a987` had to fix the consequence afterwards — a "native" soak that could silently have been an FFmpeg soak. Here the four uncovered matches were compile errors. Buffers are destroyed by us, not by vaEndPicture: va.h is explicit that the user must call vaDestroyBuffer, and the libva 0.x behaviour is long gone. Leaking two per picture at 60 fps exhausts the driver's store in minutes. pf-vaadec's presenter headroom was 4, written against no consumer. The Vulkan rung had already measured the client pipeline at four to seven held frames; it is 8 now, pinned to that crate's constant so a re-measurement moves both. Gates: macOS fmt/clippy/341 tests/cargo doc, and in the container clippy -D warnings over six crates, 795 tests, workspace check. Hardware legs are still owed — no AMD/Mesa or Intel box was reachable. |
||
|
|
00d4026054 |
Merge pull request 'Worktree field kleisty triage' (#69) from worktree-field-kleisty-triage into main
arch / build-publish (push) Failing after 40s
apple / swift (push) Successful in 1m26s
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 2m30s
deb / build-publish (push) Successful in 3m43s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
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 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
deb / build-publish-client-arm64 (push) Successful in 2m23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
ci / rust-arm64 (push) Successful in 6m51s
docker / builders-arm64cross (push) Failing after 25s
docker / deploy-docs (push) Failing after 1m57s
release / apple (push) Successful in 9m17s
deb / build-publish-host (push) Successful in 7m58s
android / android (push) Successful in 12m19s
ci / rust (push) Successful in 12m1s
flatpak / build-publish (push) Successful in 9m40s
apple / screenshots (push) Successful in 5m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 15m55s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m50s
windows-host / package (push) Canceled after 2m58s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 1s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #69 |
||
|
|
81b4f76c4d |
fix(client): a session ending on purpose stops reading as a failure
The desktop clients turned every host-side close into "Host ended the session", and a reason string means "abnormal" to everything downstream: the GTK and Windows shells raised a banner, the console overlay drew a status strip. Quitting a game you launched yourself produced all of that. Now only a host error or a lost connection carries a message; the deliberate endings return the silence those shells already give a clean exit, which is also what puts the console back in its library with nothing in the way. The Apple client gains the same distinction. It had one line for every ending — "Session ended by <host>." — which is fine for an operator stopping the session and wrong for a link that died, so each now says what happened. A game exiting stays silent and returns to the library it was launched from. Both read the reason while the connection is still up, because tearing it down is what makes it unreadable, and both fall back to their previous wording when there is no verdict — an older core, or a close that raced the read — rather than inventing a new one for a case they cannot see. |