The grey native-vulkan stream: a host re-anchor claim the client could not check #262
Merged
enricobuehler
merged 3 commits from 2026-08-16 07:57:21 +00:00
worktree-grey-frame-reanchor-fixes into main
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89fd4c6f87 |
fix(client): teach the VAAPI AV1 sizing fixture about the clean bit
apple / swift (pull_request) Successful in 2m6s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m50s
android / android (pull_request) Successful in 5m43s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m7s
ci / rust (pull_request) Failing after 5m8s
ci / rust-arm64 (pull_request) Successful in 2m21s
ci / docs-site (pull_request) Successful in 1m31s
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m34s
Adding a public field breaks LITERAL constructors, and the only one outside pf-bitstream lives in a `cfg(test)` fixture in a crate that consumes the plan types through pf-vaadec's re-exports — so nothing on the macOS side and no single-crate test run could see it. The Linux `--all-targets` gate did. Vacuously `true`: the fixture codes a key frame, which predicts from nothing. It exists to exercise the sizing path (sequence max vs coded vs render), so the clean bit is incidental here — but it still has to state the honest value, because `false` is the answer that withholds a re-anchor. |
||
|
|
93c1ed0723 |
fix(host): an RFI anchor could be picked over damage the client had already reported
The slot-family RFI backends choose a recovery anchor over `slot_wire`, which answers "did the client RECEIVE this frame" when the question is "did the client DECODE it intact". The taint sweep exists precisely to bridge that gap -- rfi.rs says so -- but it only ever runs inside invalidate_ref_frames, reachable from exactly ONE of the client's five damage signals (the frame-index gap). The other four send a plain keyframe request, which sets force_kf and taints nothing. That is self-healing while the IDR is actually emitted. It is not when the request is coalesced away by the 750 ms IDR cooldown: the client's damage then goes unrepaired AND unrecorded, and those references stay anchor candidates for the next loss -- so the host serves an anchor over damage the client already told it about, tagged as the client's definitive clean re-anchor. The client-side half of this fix now refuses such an anchor; this is the other half, which stops it being offered. Adds Encoder::distrust_references (defaulted no-op, forwarded through TrackedEncoder -- unforwarded it would have been a silent no-op for every session), implemented by the three slot-family backends through their own persistence markers, which rfi.rs explicitly says not to harmonize: Vulkan Video blanks slot_wire ONLY and leaves slot_poc naming every resident, or build_h265_rps_s0 stops retaining them and a conforming decoder evicts pictures the encoder still references -- a separate grey bug that file already documents. AMF clears its mirror slot; QSV raises ltr_tainted rather than clearing its mirror, because the RejectedRefList only names Some slots and a cleared entry would skip the very reference being distrusted. Called on the IDR-cooldown branch, where the client is still reporting damage and nothing in the table is honestly known-good until the in-flight IDR lands. Deliberately NOT on the RFI-echo branch while its budget holds: that branch's premise is that the request echoes the loss the RFI just repaired, and distrusting on the first echo would poison the table after EVERY successful recovery, so RFI could never fire twice running and a sustained-loss session would fall back to the IDR path this block exists to keep it off. RFI_ECHO_MAX_SWALLOWED is already the hedge for that premise being wrong: when the client keeps asking past the budget, the anchor demonstrably did not heal it, and the escalation arm withdraws trust then -- on evidence rather than on suspicion. Distrust never touches prediction (that runs off slot indices, not the wire domain) and all three markers self-correct within a few frames, so the suppression is brief by construction and never spans a session. |
||
|
|
26d06195f5 |
fix(client): a host recovery anchor lifted the freeze onto a grey picture, unchecked
Field report: the native-vulkan HEVC stream goes grey with moving artifacts after a host context change (starting a game), and recovers by itself in 0.5-2 s. Of the three signals that lift the post-loss freeze, two are self-evident to the client and one is pure hearsay. An IDR predicts from nothing. A recovery mark is half a re-anchor and the gate says so by requiring two. But USER_FLAG_RECOVERY_ANCHOR is the HOST asserting a fact about the CLIENT's decoder -- "the picture I coded this P-frame against is one you still hold, intact" -- and the gate took it on faith, on the first occurrence, with no scrutiny at all. The host derives that claim from bookkeeping that tracks what the client RECEIVED, not what it managed to DECODE. Those diverge exactly when the client had to conceal, and then the anchor lifts the freeze onto a picture predicted from damage AND LEAVES IT LIFTED -- so the grey plate reaches the screen and keeps reaching it until some later signal re-arms and the 500 ms backstop extracts a real IDR. That is the observed 0.5-2 s, and it is the worst-shaped failure in the module: a re-anchor claim the client can refute is worse than no claim, because no claim merely holds. So corroborate it. pf-bitstream's planners now carry a per-picture clean bit -- damage propagates down the prediction chain, because the descendants of a concealed picture raise no warning of their own -- surfaced as PicturePlan::references_clean and carried to the consumer on DecodedVkFrame. The gate gains AnchorEvidence and on_decoded_corroborated, and refuses an anchor whose references the client can prove were damaged. Refusing can only ever hold LONGER: the freeze stays up, the backstop fires on its ORIGINAL deadline, and the client escalates to the IDR the anchor failed to be. Lanes with no local parser pass Unavailable and are bit-for-bit unchanged -- all 29 pre-existing reanchor tests pass untouched. Also brings the H.264 decoder to parity with its two siblings, found on the way. It was the only one of the three that failed OPEN: a DPB slot with no bound image was traced and decoded anyway (H.265 and AV1 return UnboundReferenceSlot), reference_count was computed after the held-slot loop so a dropped reference silently took an unrelated slot's picture in its place, and there was no RecoveryLatch, so a failure left the planner DPB, the slot map and the image bindings disagreeing forever. None of it raised a warning, so the frame was shipped, presented, and cleared the demotion streak on its way past. Both latches landed with the HEVC and AV1 decoders in August; H.264 predates them and was never retro-fitted. The damage classification moves onto the warning enums so the planner's ledger and the client's concealment test cannot drift apart -- still exactly one list, now in the crate that owns the enum, with pf_vkdecode::is_integrity_warning* delegating. Every copy stays an exhaustive match with no wildcard: a future variant must stop the compiler, never default to clean. |