Two field reporters described the same picture on a Steam Deck — grey, with fresh motion painted
over it, static regions stuck — and one of them had Moonlight clean on the same host. Every
counter we own read zero while that was on screen, and the golden parity legs stayed green,
because they decode vendored conformance vectors: short, feature-sparse streams that no punktfunk
host ever emits. The one class of defect that matters here is the one none of them can see, so
the rung was effectively unfalsifiable in the field.
This adds the leg that can see it, and then reports what it saw.
What is here
field_h265_stream_writes_frame_hashes_for_ffmpeg_diff decodes any PUNKTFUNK_DUMP_VIDEO
capture on the real GPU and writes one SHA-256 per delivered frame, in display order, to <stream>.pfhash. scripts/vkdecode-field-parity.sh does the same with ffmpeg's software
decoder over the identical bytes and names the FIRST divergent frame — the only number that
localises this class, since everything after it may be downstream of one bad picture.
It is shaped for field captures rather than fixtures: it honours the .idx sidecar (skipping complete == 0 lines, because the native lanes are only ever fed whole AUs), finds the join
point the way the client did — plan until an AU succeeds — tolerates and counts per-AU errors
instead of failing the run, and releases off-size frames unshown when a capture spans a
renegotiation. PF_VKD_FIELD_YUV=<n,m> dumps named frames' planes once the diff has named them.
The second commit is what makes a result readable. A capture taken over a lossy link holds AUs
whose references never arrived, and ffmpeg conceals those differently from us — so a divergence
at one of them says nothing about this decoder. The leg records which AUs needed concealment and
prints the verdict last, so the run answers either "no AU needed concealment — any divergence is
OURS" or "a divergence at or after frame N is likely ffmpeg concealing differently".
The third commit fixes a bug in the script itself: it extracted the digest by matching a sha256= prefix that ffmpeg's framehash muxer does not emit — its rows are 0, <dts>, <pts>, <duration>, <size>, <bare hex>, with the algorithm named once in a #hash:
header. The reference file came out EMPTY and the script reported "ffmpeg=0 frames", which reads
exactly like ffmpeg having failed to decode the capture.
What it found
Run on the reporting Deck itself — AMD Custom GPU 0932 (RADV VANGOGH), Mesa 26.0.0-devel, mode=distinct.
A clean capture (5 850 AUs, no concealment): 5 850 / 5 850 frames bit-identical to
libavcodec. Zero AU errors, zero off-size frames, and the frame count matched the session's own total_frames exactly.
The capture taken while the grey was on screen (55 478 AUs, genuine 5 % packet loss):
Frames 0 – 45 271
0 mismatches
First divergence
frame 45 272
First AU needing concealment
45 272
The divergence begins at exactly the frame where a reference genuinely went missing, and not one
frame earlier. The 2 293 mismatches that follow fall into bounded ranges that each open at a
concealed AU and then heal at the next IDR — two decoders concealing a missing reference
differently, drifting, and re-converging.
Together that is 51 122 consecutive frames of real host stream, bit-identical to libavcodec, on
the hardware the reports came from. The decoder does not produce wrong pixels for valid
bitstream; grey requires a missing reference. That retires the decoder, the parameter-set
conversion, the DPB bookkeeping and the driver as suspects, and moves the investigation to why
references go missing in the first place.
What it does not settle: the harness is SERIALIZED (fence wait plus readback per frame) while the
client PIPELINES, so a decode/present timing race would not reproduce here. That remains the one
untested mechanism.
Gates
cargo fmt --check, clippy --all-targets and cargo test --lib clean on pf-vkdecode (197
tests); bash -n on the script; check-docs-drift.sh exits 0 (its PUNKTFUNK_STANDBY_SINK_KEEP
note is pre-existing on main and untouched here). The golden legs were re-run on the Deck
unchanged — H.265 250/250, Main 10, and the low-delay host fixture all still bit-identical.
The #[ignore]d legs do not run in CI, as before.
User-facing fact changed? n/a. The two new variables are test-only and follow the existing PF_VKD_* convention (PF_VKD_TEST_READBACK, PF_VKD_SMOKE_VENDOR), which the docs-drift gate
does not scan — it reads PUNKTFUNK_* only. No knob, port, install step or feature behaviour
changes: this PR adds a test leg and a script and touches no shipping code path.
Two field reporters described the same picture on a Steam Deck — grey, with fresh motion painted
over it, static regions stuck — and one of them had Moonlight clean on the same host. Every
counter we own read zero while that was on screen, and the golden parity legs stayed green,
because they decode vendored conformance vectors: short, feature-sparse streams that no punktfunk
host ever emits. The one class of defect that matters here is the one none of them can see, so
the rung was effectively unfalsifiable in the field.
This adds the leg that can see it, and then reports what it saw.
## What is here
`field_h265_stream_writes_frame_hashes_for_ffmpeg_diff` decodes any `PUNKTFUNK_DUMP_VIDEO`
capture on the real GPU and writes one SHA-256 per delivered frame, in display order, to
`<stream>.pfhash`. `scripts/vkdecode-field-parity.sh` does the same with ffmpeg's software
decoder over the identical bytes and names the FIRST divergent frame — the only number that
localises this class, since everything after it may be downstream of one bad picture.
It is shaped for field captures rather than fixtures: it honours the `.idx` sidecar (skipping
`complete == 0` lines, because the native lanes are only ever fed whole AUs), finds the join
point the way the client did — plan until an AU succeeds — tolerates and counts per-AU errors
instead of failing the run, and releases off-size frames unshown when a capture spans a
renegotiation. `PF_VKD_FIELD_YUV=<n,m>` dumps named frames' planes once the diff has named them.
The second commit is what makes a result readable. A capture taken over a lossy link holds AUs
whose references never arrived, and ffmpeg conceals those differently from us — so a divergence
at one of them says nothing about this decoder. The leg records which AUs needed concealment and
prints the verdict last, so the run answers either "no AU needed concealment — any divergence is
OURS" or "a divergence at or after frame N is likely ffmpeg concealing differently".
The third commit fixes a bug in the script itself: it extracted the digest by matching a
`sha256=` prefix that ffmpeg's framehash muxer does not emit — its rows are
`0, <dts>, <pts>, <duration>, <size>, <bare hex>`, with the algorithm named once in a `#hash:`
header. The reference file came out EMPTY and the script reported "ffmpeg=0 frames", which reads
exactly like ffmpeg having failed to decode the capture.
## What it found
Run on the reporting Deck itself — AMD Custom GPU 0932 (RADV VANGOGH), Mesa 26.0.0-devel,
`mode=distinct`.
**A clean capture (5 850 AUs, no concealment):** 5 850 / 5 850 frames bit-identical to
libavcodec. Zero AU errors, zero off-size frames, and the frame count matched the session's own
`total_frames` exactly.
**The capture taken while the grey was on screen (55 478 AUs, genuine 5 % packet loss):**
| | |
|---|---|
| Frames 0 – 45 271 | **0 mismatches** |
| First divergence | frame **45 272** |
| First AU needing concealment | **45 272** |
The divergence begins at exactly the frame where a reference genuinely went missing, and not one
frame earlier. The 2 293 mismatches that follow fall into bounded ranges that each open at a
concealed AU and then heal at the next IDR — two decoders concealing a missing reference
differently, drifting, and re-converging.
Together that is **51 122 consecutive frames of real host stream, bit-identical to libavcodec, on
the hardware the reports came from**. The decoder does not produce wrong pixels for valid
bitstream; grey requires a missing reference. That retires the decoder, the parameter-set
conversion, the DPB bookkeeping and the driver as suspects, and moves the investigation to why
references go missing in the first place.
What it does not settle: the harness is SERIALIZED (fence wait plus readback per frame) while the
client PIPELINES, so a decode/present timing race would not reproduce here. That remains the one
untested mechanism.
## Gates
`cargo fmt --check`, `clippy --all-targets` and `cargo test --lib` clean on pf-vkdecode (197
tests); `bash -n` on the script; `check-docs-drift.sh` exits 0 (its `PUNKTFUNK_STANDBY_SINK_KEEP`
note is pre-existing on main and untouched here). The golden legs were re-run on the Deck
unchanged — H.265 250/250, Main 10, and the low-delay host fixture all still bit-identical.
The `#[ignore]`d legs do not run in CI, as before.
**User-facing fact changed?** n/a. The two new variables are test-only and follow the existing
`PF_VKD_*` convention (`PF_VKD_TEST_READBACK`, `PF_VKD_SMOKE_VENDOR`), which the docs-drift gate
does not scan — it reads `PUNKTFUNK_*` only. No knob, port, install step or feature behaviour
changes: this PR adds a test leg and a script and touches no shipping code path.
The golden parity legs only decode streams our hosts never emit, and the
field defect they cannot see is exactly the one two Deck reporters now
show: a smear that accumulates through the reference chain while every
integrity counter stays at zero. The full client audit (wire, planner,
params conversion, slots, scope, ring, submission, caps/session) found
no defect by inspection, so the fix needs the first divergent frame off
real silicon.
This leg turns any PUNKTFUNK_DUMP_VIDEO capture into that number:
PF_VKD_FIELD_STREAM points at the .h265 (the .idx sidecar is honoured
when present, complete==0 lines skipped), the join point is found the
way the client found it (plan until an AU succeeds), and every
delivered frame is read back and hashed to <stream>.pfhash in display
order. Per-AU errors are tolerated and counted, off-size frames from a
mid-capture renegotiation are released unshown, and PF_VKD_FIELD_YUV
dumps named frames' planes for eyes-on inspection.
scripts/vkdecode-field-parity.sh is the other half: ffmpeg -f framehash
over the same bytes (nv12/p010le match the readback's packing exactly),
first divergent index, ffprobe facts for the divergent picture.
consume_frame now returns the planes and the hashing moved to its
callers, which is the whole extent of the change to the golden legs.
A capture taken over a lossy link holds AUs whose references never
arrived, and ffmpeg conceals those differently from us — so a divergence
at one of them says nothing about this decoder. Recording the AU indices
(and printing the verdict last) is what separates "our bug" from "the
field lost packets", which is the only reading that makes a field diff
actionable. Split out of a dropped commit whose other half was wrong.
The extraction matched on `sha256=`, which ffmpeg's framehash muxer
does not emit: its data rows are `0, <dts>, <pts>, <duration>, <size>,
<bare hex>` and the algorithm is named once in a `#hash:` header. So
the reference file came out EMPTY and the script reported "ffmpeg=0
frames" — indistinguishable, to a reader, from ffmpeg having failed to
decode the capture at all. Take the last comma-separated field of every
non-comment row instead.
CI gate C counts `env::set_var` mentions per file and this file was at
its baseline of five, so the field leg's sixth copy failed the build.
Raising the baseline is what the gate tells you not to do, and it would
have been the wrong answer anyway: the six copies were the same two-line
safety argument written out six times, which is precisely the shape a
ratchet exists to discourage.
`arm_test_readback` takes the GPU lock guard BY REFERENCE. `env::set_var`
is safe to call and unsound from a live multithreaded process, so "the
caller holds the binary-wide lock" is the entire safety argument — and a
borrow states it in the type system, where it cannot drift out of date,
instead of in prose repeated at each call site.
The file drops from six mentions to two (the call, and the doc comment
that names the API it is arguing about — gate C counts comments too, by
design, and writing around its grep would be gaming it). The baseline
moves 5 -> 2 to match, so the ratchet stays tight rather than merely
satisfied.
Gates: fmt, clippy --all-targets and 197 lib tests clean;
check-unsafe-hygiene.sh gate C now passes. Gate B's punktfunk-tray
finding is untouched by this branch and fails identically on main.
`ci / rust` has been red on main: gate B of check-unsafe-hygiene.sh flags
`.unwrap()` on the status mutex inside `wndproc`, and it is right to. A
panic crossing an `extern "system"` boundary does not unwind, it ABORTS
the process — so a thread that panicked while holding this lock would
take down the host's only visible surface, on a box where the icon is
also how the operator stops the host.
Poisoning is not a reason to do that here. What the mutex guards is a
display enum; reading it after another thread died is harmless, and
`PoisonError::into_inner` hands it over — the idiom already used
elsewhere in the tree.
Routed through one `App::status()` rather than patched at the line CI
could see. The gate is lexical, so it flagged only the call written
inline in `wndproc`; the other four sit in helpers that `wndproc` calls,
where a panic unwinds into the very same boundary and aborts just the
same. Fixing only the visible one would have left four identical aborts
a function call away and made the gate look satisfied rather than the
bug fixed.
⚠ NOT verified locally: the tray cannot be type-checked for Windows from
a Mac (aws-lc-sys' build script cross-compiles C and dies in cc-rs long
before win.rs), and scripts/xcheck.sh does not carry punktfunk-tray.
windows-host.yml builds AND clippies this crate and triggers on
`crates/punktfunk-tray/**`, so CI is the verifier here.
Both this branch and #460 fixed the same abort, independently and within
the hour. #460 merged first, patching four call sites inline; this one
routes all five through one `App::status()`, which is the better shape
and also covers the writer in `run()` that the inline pass left alone.
Conflict resolved to this branch for `win.rs`. Afterwards the file holds
exactly one `lock()` — inside the accessor — so every reader goes
through it. `check-unsafe-hygiene.sh` reports all three gates clean and
rustfmt is clean under the pinned 2021 style edition.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two field reporters described the same picture on a Steam Deck — grey, with fresh motion painted
over it, static regions stuck — and one of them had Moonlight clean on the same host. Every
counter we own read zero while that was on screen, and the golden parity legs stayed green,
because they decode vendored conformance vectors: short, feature-sparse streams that no punktfunk
host ever emits. The one class of defect that matters here is the one none of them can see, so
the rung was effectively unfalsifiable in the field.
This adds the leg that can see it, and then reports what it saw.
What is here
field_h265_stream_writes_frame_hashes_for_ffmpeg_diffdecodes anyPUNKTFUNK_DUMP_VIDEOcapture on the real GPU and writes one SHA-256 per delivered frame, in display order, to
<stream>.pfhash.scripts/vkdecode-field-parity.shdoes the same with ffmpeg's softwaredecoder over the identical bytes and names the FIRST divergent frame — the only number that
localises this class, since everything after it may be downstream of one bad picture.
It is shaped for field captures rather than fixtures: it honours the
.idxsidecar (skippingcomplete == 0lines, because the native lanes are only ever fed whole AUs), finds the joinpoint the way the client did — plan until an AU succeeds — tolerates and counts per-AU errors
instead of failing the run, and releases off-size frames unshown when a capture spans a
renegotiation.
PF_VKD_FIELD_YUV=<n,m>dumps named frames' planes once the diff has named them.The second commit is what makes a result readable. A capture taken over a lossy link holds AUs
whose references never arrived, and ffmpeg conceals those differently from us — so a divergence
at one of them says nothing about this decoder. The leg records which AUs needed concealment and
prints the verdict last, so the run answers either "no AU needed concealment — any divergence is
OURS" or "a divergence at or after frame N is likely ffmpeg concealing differently".
The third commit fixes a bug in the script itself: it extracted the digest by matching a
sha256=prefix that ffmpeg's framehash muxer does not emit — its rows are0, <dts>, <pts>, <duration>, <size>, <bare hex>, with the algorithm named once in a#hash:header. The reference file came out EMPTY and the script reported "ffmpeg=0 frames", which reads
exactly like ffmpeg having failed to decode the capture.
What it found
Run on the reporting Deck itself — AMD Custom GPU 0932 (RADV VANGOGH), Mesa 26.0.0-devel,
mode=distinct.A clean capture (5 850 AUs, no concealment): 5 850 / 5 850 frames bit-identical to
libavcodec. Zero AU errors, zero off-size frames, and the frame count matched the session's own
total_framesexactly.The capture taken while the grey was on screen (55 478 AUs, genuine 5 % packet loss):
The divergence begins at exactly the frame where a reference genuinely went missing, and not one
frame earlier. The 2 293 mismatches that follow fall into bounded ranges that each open at a
concealed AU and then heal at the next IDR — two decoders concealing a missing reference
differently, drifting, and re-converging.
Together that is 51 122 consecutive frames of real host stream, bit-identical to libavcodec, on
the hardware the reports came from. The decoder does not produce wrong pixels for valid
bitstream; grey requires a missing reference. That retires the decoder, the parameter-set
conversion, the DPB bookkeeping and the driver as suspects, and moves the investigation to why
references go missing in the first place.
What it does not settle: the harness is SERIALIZED (fence wait plus readback per frame) while the
client PIPELINES, so a decode/present timing race would not reproduce here. That remains the one
untested mechanism.
Gates
cargo fmt --check,clippy --all-targetsandcargo test --libclean on pf-vkdecode (197tests);
bash -non the script;check-docs-drift.shexits 0 (itsPUNKTFUNK_STANDBY_SINK_KEEPnote is pre-existing on main and untouched here). The golden legs were re-run on the Deck
unchanged — H.265 250/250, Main 10, and the low-delay host fixture all still bit-identical.
The
#[ignore]d legs do not run in CI, as before.User-facing fact changed? n/a. The two new variables are test-only and follow the existing
PF_VKD_*convention (PF_VKD_TEST_READBACK,PF_VKD_SMOKE_VENDOR), which the docs-drift gatedoes not scan — it reads
PUNKTFUNK_*only. No knob, port, install step or feature behaviourchanges: this PR adds a test leg and a script and touches no shipping code path.