fix(encode/windows): a stale PUNKTFUNK_ENCODER pin no longer wedges a conflicting GPU selection

On a hybrid box, picking a GPU in the web console whose vendor contradicts a
host.env PUNKTFUNK_ENCODER pin produced an unrecoverable session: the pin won
backend selection while the adapter followed the console, the wrong-vendor
encoder failed deterministically at submit, the reset ladder burned its 5
in-place rebuilds on it, and the client reconnected into the identical wall
forever (~10 s per cycle, no visible reason).

Three legs:
- windows_resolved_backend() now reconciles: a hardware pin whose vendor
  contradicts the selected GPU is overridden by the adapter-derived backend
  (capture + encode share one adapter, so honoring the pin can only fail);
  open_video warns loudly when a pin loses. The reconciliation is a pure,
  unit-tested table (resolve_windows_backend).
- the QSV wrong-adapter bind is typed TerminalEncoderError, and the stream
  loop's reset ladder ends the session immediately on it instead of feeding a
  deterministic config error 5 futile rebuilds.
- the un-pinned path was already correct (verified on-glass: NVIDIA preference
  + no pin = stable AV1 10-bit HDR on NVENC), so the override simply takes the
  conflict case onto that path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 16:53:19 +02:00
co-authored by Claude Fable 5
parent 36e566052f
commit 751d1de506
3 changed files with 181 additions and 39 deletions
@@ -2329,6 +2329,19 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
// assign next. The RFI backends pin their frame numbering to it.
let wire_index = au_seq.wrapping_add(inflight.len() as u32);
if let Err(e) = enc.submit_indexed(&frame, wire_index) {
// A typed-terminal error is a deterministic configuration failure — the identical
// wall on every attempt, so rebuilds can't help. End the session at once with the
// carried cause (observed: a stale PUNKTFUNK_ENCODER pin vs. the selected adapter
// burned all 5 rebuilds per connect while the client reconnected forever).
if e.downcast_ref::<crate::encode::TerminalEncoderError>()
.is_some()
{
tracing::error!(
error = %format!("{e:#}"),
"encoder failed with a deterministic configuration error — ending the video \
session without rebuild attempts (see the error for the remedy)");
return Err(e).context("encoder submit");
}
// The input half of an encode stall: once the driver stops draining AUs, libavcodec's
// one-frame buffer fills and avcodec_send_frame starts failing (EAGAIN) — the same
// wedge the watchdog below catches, seen from submit. Rebuild the encoder in place