fix(host): dual identical GPUs stream again — self-heal the IDD-push ring onto the driver's real render adapter
ci / web (push) Successful in 50s
apple / swift (push) Successful in 1m13s
ci / docs-site (push) Successful in 1m5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 20s
apple / screenshots (push) Successful in 5m35s
windows-host / package (push) Successful in 7m57s
deb / build-publish (push) Successful in 12m0s
arch / build-publish (push) Successful in 14m51s
android / android (push) Successful in 16m21s
ci / rust (push) Successful in 17m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m0s

A box with two identical GPUs (twin RTX 4090s in the field report) died out
of pipeline retries with driver_status=2 (DRV_STATUS_TEX_FAIL, detail
0x80070057): SET_RENDER_ADAPTER pinned the driver to one twin at monitor ADD,
but the ring open re-ran the GPU picker, whose max-VRAM tie is settled by
DXGI enumeration order — which follows the primary display that the vdisplay
flow itself moves mid-session. Ring on one twin, swap-chain on the other →
the driver can't open the shared textures, and every retry repeats the same
wrong pick.

- wait_for_attach: TEX_FAIL becomes a typed AttachTexFail carrying the render
  LUID the driver reports in the shared frame header (written before the
  texture opens, so valid on failure) — the error now names both adapters
  instead of guessing "mismatch?".
- open_inner: on AttachTexFail with a different, non-zero driver LUID, rebind
  the ring there and reopen ONCE. Both candidates are real GPU adapters, so
  NVENC keeps getting a device it accepts.
- gpu::enumerate (Windows): sort the inventory by LUID so the max-VRAM tie,
  the env-substring first match, and twin occurrence numbering are stable for
  the boot instead of tracking the primary display.
- manager: record the ADD-time render pin on Monitor and warn on reuse when
  the current pick has moved away from it (the pick only takes effect on the
  next monitor create; /display/release forces one).
- pf_vdisplay: drop the "ADD render adapter DIFFERS from pinned" warn — the
  ADD reply carries IDARG_OUT_MONITORARRIVAL.OsAdapterLuid (the IddCx DISPLAY
  adapter, verified on-glass), not the render GPU, so the check compared
  unrelated LUIDs and fired on every ADD.

Verified on the RTX 4090 box: single-GPU streaming is unchanged (IddPush +
NVENC AV1 session, no new warnings). The rebind path needs a twin-GPU box —
until validated there, /display/release remains the manual recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 02:06:08 +02:00
parent 08694b4026
commit b7cb75a48a
4 changed files with 158 additions and 30 deletions
@@ -71,7 +71,9 @@ pub(crate) trait VdisplayDriver: Send + Sync {
unsafe fn open(&self, reap_orphans: bool) -> Result<(OwnedHandle, u32)>;
/// ADD a virtual monitor at `mode`, pinning the IDD render GPU to `render_luid` first if `Some`, and
/// requesting `preferred_monitor_id` (the host's per-client stable id; `0` = auto). Returns the REMOVE
/// key + target id + the adapter LUID the driver actually used.
/// key + target id + the IddCx DISPLAY adapter LUID from the ADD reply
/// (`IDARG_OUT_MONITORARRIVAL.OsAdapterLuid` — NOT the render GPU; the driver reports its render
/// adapter only in the shared frame header).
///
/// # Safety
/// `dev` must be the live control handle from [`open`](Self::open).
@@ -100,7 +102,14 @@ pub(crate) trait VdisplayDriver: Send + Sync {
struct Monitor {
key: MonitorKey,
target_id: u32,
/// IddCx DISPLAY adapter LUID from the ADD reply (`IDARG_OUT_MONITORARRIVAL.OsAdapterLuid`) —
/// NOT the render GPU the driver renders on (the driver reports that one in the shared frame
/// header only). Do not compare it against render-GPU picks.
luid: LUID,
/// The render-GPU pin handed to SET_RENDER_ADAPTER at this monitor's ADD (`None` = no GPU was
/// selectable). The pin is never re-issued on reuse, so this is what the driver still renders
/// on — [`warn_if_pick_moved`] compares the CURRENT pick against it.
render_pin: Option<LUID>,
/// The driver's WUDFHost pid (from the ADD reply) — carried into [`WinCaptureTarget`] so the
/// IDD-push capturer knows where to duplicate the sealed frame channel's handles.
wudf_pid: u32,
@@ -475,6 +484,7 @@ impl VirtualDisplayManager {
backend = self.driver.name(),
"virtual monitor reused (concurrent / reconfigure session)"
);
warn_if_pick_moved(mon);
return Ok(self.output_for(mon, quit));
}
@@ -487,6 +497,7 @@ impl VirtualDisplayManager {
backend = self.driver.name(),
"virtual monitor reused (reconnect to a kept monitor)"
);
warn_if_pick_moved(&mon);
if mon.mode != mode {
// SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live
// display topology. `mon` is the local monitor just moved out of the `Lingering`
@@ -562,13 +573,14 @@ impl VirtualDisplayManager {
crate::vdisplay::policy::Identity::PerClient,
)
.unwrap_or(0);
let render_pin = resolve_render_pin();
// SAFETY: `create_monitor`'s own `# Safety` contract guarantees `dev` is the live control
// handle; we forward it unchanged to `add_monitor`, whose precondition is exactly that.
// `resolve_render_pin()` returns an `Option<LUID>` by value (plain `Copy`), so no borrowed
// memory crosses the call.
// `render_pin` is an `Option<LUID>` by value (plain `Copy`), so no borrowed memory
// crosses the call.
let added = unsafe {
self.driver
.add_monitor(dev, mode, resolve_render_pin(), preferred_id)?
.add_monitor(dev, mode, render_pin, preferred_id)?
};
// Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down.
@@ -724,6 +736,7 @@ impl VirtualDisplayManager {
key: added.key,
target_id: added.target_id,
luid: added.luid,
render_pin,
wudf_pid: added.wudf_pid,
gdi_name,
mode,
@@ -1033,6 +1046,36 @@ fn resolve_render_pin() -> Option<LUID> {
crate::win_adapter::resolve_render_adapter_luid()
}
/// A reused monitor keeps the render GPU the driver was pinned to at its ADD — the pin is never
/// re-issued on reuse. When the current pick has moved since then (an operator preference change,
/// or the max-VRAM tie shifting on identical twin GPUs), say so: the session self-heals (the
/// IDD-push open rebinds its ring to the driver's actual adapter on a mismatch), but the new pick
/// only takes effect on the next monitor CREATE, which the mgmt `/display/release` forces.
/// Compares the pick against the ADD-time PIN — `mon.luid` is the IddCx display adapter and must
/// not be compared with render-GPU picks.
fn warn_if_pick_moved(mon: &Monitor) {
let Some(pin) = mon.render_pin else { return };
let Some(sel) = crate::gpu::selected_gpu() else {
return;
};
let pick = sel.info.luid();
if (pick.LowPart, pick.HighPart) != (pin.LowPart, pin.HighPart) {
tracing::warn!(
pinned_adapter = format!("{:08x}:{:08x}", pin.HighPart, pin.LowPart),
current_pick = format!(
"{:08x}:{:08x} ({}, {})",
pick.HighPart,
pick.LowPart,
sel.info.name,
sel.source.tag()
),
"reused virtual monitor is pinned to a different render GPU than the current pick — \
the session follows the pinned GPU; free the display (mgmt /display/release) to \
recreate it on the picked one"
);
}
}
/// A read-only view of the managed monitor for the mgmt `/display/state` endpoint (Goal:
/// display-management registry facade). Backend-neutral; the [`crate::vdisplay::registry`] facade
/// maps it into the wire shape.
@@ -539,17 +539,12 @@ impl VdisplayDriver for PfVdisplayDriver {
);
}
}
if let Some(pin) = render_luid {
if luid.LowPart == pin.LowPart && luid.HighPart == pin.HighPart {
tracing::info!("pf-vdisplay ADD render adapter matches the pinned GPU (pin took)");
} else {
tracing::warn!(
add = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
pinned = format!("{:08x}:{:08x}", pin.HighPart, pin.LowPart),
"pf-vdisplay ADD render adapter DIFFERS from pinned — driver ignored SET_RENDER_ADAPTER?"
);
}
}
// NOTE: `reply.adapter_luid` is the IddCx DISPLAY adapter
// (`IDARG_OUT_MONITORARRIVAL.OsAdapterLuid`), NOT the render GPU, so it can NOT validate
// SET_RENDER_ADAPTER — a comparison against the pin here fired "DIFFERS from pinned" on
// every ADD (verified on-glass: reply 0x22c05 vs pin 0x15b05 on a single-4090 box). The
// driver reports its ACTUAL render adapter in the shared frame header; the IDD-push
// capturer checks it there and rebinds on a mismatch.
Ok(AddedMonitor {
key: MonitorKey::Session(session_id),
target_id: reply.target_id,