AMD Strix Halo Windows client streaming from a 4090 host: black rectangular blocks "moving like an equalizer from top to bottom", identical with Vulkan, DXVA, and CPU decoders, while an Android client on the same host streams clean (both sides 0.31.4).
Identical across all three decoders means the AU bytes are wrong before any decoder sees them — the three lanes take three different presenter paths, so the presenter is exonerated too. And the one negotiated difference from Android is VIDEO_CAP_MULTI_SLICE (unconditional on desktop, gated on multiSliceTolerant() on Android), which arms the slice-granularity streamed wire: NVENC sub-frame readback, per-slice FEC blocks at sender-declared base offsets.
The gap
A slice sentinel's wire base was bounds-checked (shard-aligned, in range, below the final block's base — plus the retro-validation when the final block pins the totals) but nothing ever proved the blocks tile the AU. A base that lies within bounds leaves a zero gap at the honest offset and an overlap at the claimed one — and the reassembler still stamped the frame complete: true. The decoder paints the wrong-offset bytes as garbage rectangles, no loss counter moves, so the freeze gate never arms, no recovery keyframe is ever requested, and the intra-refresh wave marches the corruption down the picture indefinitely. The old comment said a lying base "can only corrupt this frame's own pixels" — true for memory safety, but that corrupt frame then shipped as complete.
The fix
At frame completion, verify the slice blocks tile [0..total_data] contiguously. A frame that doesn't is killed exactly like a lying final header already is: index remembered against stragglers, counted into frames_dropped — which is the signal the session pump already treats as loss, arming the freeze gate and requesting recovery. The user sees a ~100 ms freeze-and-heal instead of persistent black bands. The slice-progressive prefix walk gets the same one-line contiguity guard so a lying base can't extend a "contiguous" prefix either.
The new test forges a base that passes every pre-fix check (block 1 shifted one shard: still aligned, still below the final base) and asserts the frame is refused, counted lost, releases its budget, and can't be resurrected. Verified by mutation: without the guard, that frame is delivered as complete — today's shipping behavior.
What this does and doesn't cover
This makes the client refuse to render the corruption and recover instead, whichever host bug produces mis-based blocks. The host-side cause on the 4090 still wants its own fix — the leading suspect is the NVENC sub-frame chunk reader, whose emitted-prefix consistency check is #[cfg(debug_assertions)] only (nvenc.rs:2251). Host-side bisect knobs for the reporter, in order: PUNKTFUNK_NVENC_SUBFRAME=0 → PUNKTFUNK_SLICE_STREAM=0 → PUNKTFUNK_NVENC_SLICES=1, plus PUNKTFUNK_DUMP_VIDEO=<dir> on the client for ground truth.
## The field report
AMD Strix Halo Windows client streaming from a 4090 host: black rectangular blocks "moving like an equalizer from top to bottom", **identical with Vulkan, DXVA, and CPU decoders**, while an Android client on the same host streams clean (both sides 0.31.4).
Identical across all three decoders means the AU bytes are wrong before any decoder sees them — the three lanes take three different presenter paths, so the presenter is exonerated too. And the one negotiated difference from Android is `VIDEO_CAP_MULTI_SLICE` (unconditional on desktop, gated on `multiSliceTolerant()` on Android), which arms the slice-granularity streamed wire: NVENC sub-frame readback, per-slice FEC blocks at sender-declared base offsets.
## The gap
A slice sentinel's wire base was bounds-checked (shard-aligned, in range, below the final block's base — plus the retro-validation when the final block pins the totals) but nothing ever proved the blocks **tile** the AU. A base that lies *within* bounds leaves a zero gap at the honest offset and an overlap at the claimed one — and the reassembler still stamped the frame `complete: true`. The decoder paints the wrong-offset bytes as garbage rectangles, **no loss counter moves**, so the freeze gate never arms, no recovery keyframe is ever requested, and the intra-refresh wave marches the corruption down the picture indefinitely. The old comment said a lying base "can only corrupt this frame's own pixels" — true for memory safety, but that corrupt frame then shipped as complete.
## The fix
At frame completion, verify the slice blocks tile `[0..total_data]` contiguously. A frame that doesn't is killed exactly like a lying final header already is: index remembered against stragglers, counted into `frames_dropped` — which is the signal the session pump already treats as loss, arming the freeze gate and requesting recovery. The user sees a ~100 ms freeze-and-heal instead of persistent black bands. The slice-progressive prefix walk gets the same one-line contiguity guard so a lying base can't extend a "contiguous" prefix either.
The new test forges a base that passes every pre-fix check (block 1 shifted one shard: still aligned, still below the final base) and asserts the frame is refused, counted lost, releases its budget, and can't be resurrected. Verified by mutation: without the guard, that frame is delivered as `complete` — today's shipping behavior.
## What this does and doesn't cover
This makes the client refuse to render the corruption and recover instead, whichever host bug produces mis-based blocks. The *host-side* cause on the 4090 still wants its own fix — the leading suspect is the NVENC sub-frame chunk reader, whose emitted-prefix consistency check is `#[cfg(debug_assertions)]` only (`nvenc.rs:2251`). Host-side bisect knobs for the reporter, in order: `PUNKTFUNK_NVENC_SUBFRAME=0` → `PUNKTFUNK_SLICE_STREAM=0` → `PUNKTFUNK_NVENC_SLICES=1`, plus `PUNKTFUNK_DUMP_VIDEO=<dir>` on the client for ground truth.
A slice-streamed sentinel's wire base was bounds-checked (in range, below
the final block) but never validated to TILE the AU: a base that lied
within bounds left a zero gap and an overlap, and the reassembler still
stamped the frame `complete`. The decoder then painted the wrong-offset
bytes as garbage rectangles while no loss counter moved — so the recovery
machinery never fired and the corruption marched on indefinitely (the
field report: black bars 'moving like an equalizer', identical across
Vulkan/DXVA/CPU decoders, Android immune because it never negotiates the
slice wire).
At completion, verify the blocks tile [0..total_data] contiguously; a
frame that doesn't is killed exactly like a lying final header — index
remembered against stragglers, counted into frames_dropped, which is the
signal that arms the freeze gate and requests recovery. The
slice-progressive prefix walk gets the same contiguity guard so a lying
base can't extend a 'contiguous' prefix either.
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.
The field report
AMD Strix Halo Windows client streaming from a 4090 host: black rectangular blocks "moving like an equalizer from top to bottom", identical with Vulkan, DXVA, and CPU decoders, while an Android client on the same host streams clean (both sides 0.31.4).
Identical across all three decoders means the AU bytes are wrong before any decoder sees them — the three lanes take three different presenter paths, so the presenter is exonerated too. And the one negotiated difference from Android is
VIDEO_CAP_MULTI_SLICE(unconditional on desktop, gated onmultiSliceTolerant()on Android), which arms the slice-granularity streamed wire: NVENC sub-frame readback, per-slice FEC blocks at sender-declared base offsets.The gap
A slice sentinel's wire base was bounds-checked (shard-aligned, in range, below the final block's base — plus the retro-validation when the final block pins the totals) but nothing ever proved the blocks tile the AU. A base that lies within bounds leaves a zero gap at the honest offset and an overlap at the claimed one — and the reassembler still stamped the frame
complete: true. The decoder paints the wrong-offset bytes as garbage rectangles, no loss counter moves, so the freeze gate never arms, no recovery keyframe is ever requested, and the intra-refresh wave marches the corruption down the picture indefinitely. The old comment said a lying base "can only corrupt this frame's own pixels" — true for memory safety, but that corrupt frame then shipped as complete.The fix
At frame completion, verify the slice blocks tile
[0..total_data]contiguously. A frame that doesn't is killed exactly like a lying final header already is: index remembered against stragglers, counted intoframes_dropped— which is the signal the session pump already treats as loss, arming the freeze gate and requesting recovery. The user sees a ~100 ms freeze-and-heal instead of persistent black bands. The slice-progressive prefix walk gets the same one-line contiguity guard so a lying base can't extend a "contiguous" prefix either.The new test forges a base that passes every pre-fix check (block 1 shifted one shard: still aligned, still below the final base) and asserts the frame is refused, counted lost, releases its budget, and can't be resurrected. Verified by mutation: without the guard, that frame is delivered as
complete— today's shipping behavior.What this does and doesn't cover
This makes the client refuse to render the corruption and recover instead, whichever host bug produces mis-based blocks. The host-side cause on the 4090 still wants its own fix — the leading suspect is the NVENC sub-frame chunk reader, whose emitted-prefix consistency check is
#[cfg(debug_assertions)]only (nvenc.rs:2251). Host-side bisect knobs for the reporter, in order:PUNKTFUNK_NVENC_SUBFRAME=0→PUNKTFUNK_SLICE_STREAM=0→PUNKTFUNK_NVENC_SLICES=1, plusPUNKTFUNK_DUMP_VIDEO=<dir>on the client for ground truth.