refactor(pf-encode): extract the slot-family RFI recovery policy (WP7.2)
AMF (user-LTR bitfield), QSV (mfxExtRefListCtrl) and Vulkan Video (the
app-owned DPB slot table) each hand-implemented the same loss-recovery
decision: distrust every reference encoded at-or-after the loss start,
anchor on the newest one strictly older. Three copies had already
diverged once — the fecbec2d taint sweep reached AMF/QSV a commit before
the Vulkan backend was carved out, and Vulkan shipped without it. The
decision now lives once in enc/rfi.rs, pure and unit-tested (this path
had zero coverage and is the loss-recovery path); every mechanism —
how a force is applied, how distrust is persisted — stays in its
backend.
Behavior-preservation notes, each critic-verified against the shipped
code:
- Callers feed only currently-trusted references; taint (>= loss) and
anchor (< loss) predicates are disjoint, so pre-sweep-view pick ==
post-sweep-table pick on every input. Tie-break preserved (first
entry wins == the strict '>' all three used, ascending slot order).
- plan_slot_recovery delegates its pick half to pick_anchor, which
keeps both items live on every leg (pick_anchor's only external
caller is Linux+vulkan-encode; dead_code is an item lint) and makes
'anchor chosen from the taint snapshot' structural.
- The decline arms are deliberately NOT uniform and stay put: AMF/QSV
clear an un-consumed pending_force; Vulkan leaves pending_loss armed
(a stale arm re-resolves at frame-build into the healing IDR) — now
pinned in comments on both sides.
- Vulkan's frame-build site is a pick-only re-run by design (the arm
carries the loss start, not the slot); pick_recovery_slot is deleted
and its tests migrated 1:1 into the shared module.
Test-count deltas are expected: the rfi tests now run on every Windows
combo (amf.rs is featureless there) and on the Linux feature leg.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1416,36 +1416,30 @@ impl Encoder for QsvEncoder {
|
||||
if !self.ltr_active || first < 0 || first > last {
|
||||
return false;
|
||||
}
|
||||
// Taint sweep BEFORE picking the anchor: an LTR marked at-or-after the loss start was
|
||||
// encoded inside the client's corrupt window — the client either never received it or
|
||||
// decoded it against a broken reference chain. Serving it as "known-good" on a LATER
|
||||
// loss ships corruption as the recovery anchor, and every subsequent mark re-samples
|
||||
// the soup — the sustained-loss field failure where the picture never healed. Dropped
|
||||
// slots stay dropped; the cadence re-marks a clean frame within ~1/4 s.
|
||||
//
|
||||
// Mark tainted rather than clearing: `ltr_slots` mirrors the HARDWARE DPB, and nulling an
|
||||
// entry issues no VPL call — the frame stays marked long-term in the encoder. Clearing it
|
||||
// made the rejection list below (which iterates the post-sweep mirror and only names `Some`
|
||||
// slots) silently SKIP the one entry the sweep exists to distrust, so the recovery frame
|
||||
// could still predict from it. With two slots the "exactly one swept" case is the modal
|
||||
// one, and it was the broken one.
|
||||
for (slot, marked) in self.ltr_slots.iter().enumerate() {
|
||||
if marked.is_some_and(|idx| idx >= first) {
|
||||
self.ltr_tainted[slot] = true;
|
||||
// The taint-sweep + anchor-pick POLICY lives in `rfi::plan_slot_recovery` (one decision
|
||||
// shared with AMF and Vulkan Video). This backend's mechanism: distrust is a SEPARATE
|
||||
// `ltr_tainted` flag, never a cleared mirror slot — `ltr_slots` mirrors the HARDWARE DPB,
|
||||
// and nulling an entry issues no VPL call, so the frame stays marked long-term in the
|
||||
// encoder. Clearing it made the rejection list below (which iterates the mirror and only
|
||||
// names `Some` slots) silently SKIP the one entry the sweep exists to distrust, so the
|
||||
// recovery frame could still predict from it. With two slots the "exactly one swept" case
|
||||
// is the modal one, and it was the broken one. The `!ltr_tainted` view filter below is
|
||||
// what persists that distrust across loss events (this call's taints are excluded from
|
||||
// this call's anchor by the policy itself).
|
||||
let view: Vec<(usize, i64)> = self
|
||||
.ltr_slots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(slot, _)| !self.ltr_tainted[slot])
|
||||
.filter_map(|(s, m)| m.map(|w| (s, w)))
|
||||
.collect();
|
||||
let plan = super::rfi::plan_slot_recovery(&view, first);
|
||||
for (slot, tainted) in self.ltr_tainted.iter_mut().enumerate() {
|
||||
if plan.tainted & (1 << slot) != 0 {
|
||||
*tainted = true;
|
||||
}
|
||||
}
|
||||
let mut best: Option<(usize, i64)> = None;
|
||||
for (slot, marked) in self.ltr_slots.iter().enumerate() {
|
||||
if self.ltr_tainted[slot] {
|
||||
continue; // still in the DPB, but encoded inside the corrupt window
|
||||
}
|
||||
if let Some(idx) = *marked {
|
||||
if idx < first && best.is_none_or(|(_, b)| idx > b) {
|
||||
best = Some((slot, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
match best {
|
||||
match plan.anchor {
|
||||
Some((slot, ltr_frame)) => {
|
||||
self.pending_force = Some(slot);
|
||||
tracing::info!(
|
||||
|
||||
Reference in New Issue
Block a user