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
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:
@@ -760,6 +760,56 @@ impl IddPushCapturer {
|
||||
target: WinCaptureTarget,
|
||||
preferred: Option<(u32, u32, u32)>,
|
||||
client_10bit: bool,
|
||||
) -> Result<Self> {
|
||||
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
|
||||
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
|
||||
// ADD, so on a healthy box they agree, and NVENC gets a device on a real GPU adapter.
|
||||
// (`target.adapter_luid` is NOT that adapter: the ADD reply carries
|
||||
// `IDARG_OUT_MONITORARRIVAL.OsAdapterLuid` = the IddCx DISPLAY adapter — verified
|
||||
// on-glass; it stays a last-resort fallback for a pickerless box only.) When the pick and
|
||||
// the driver HAVE drifted — identical twin GPUs whose max-VRAM tie moved between ADD and
|
||||
// this open, or a stale kept monitor across an adapter re-init — the driver reports
|
||||
// TEX_FAIL plus the adapter it actually renders on, and the rebind below reopens on that.
|
||||
let luid = crate::win_adapter::resolve_render_adapter_luid().unwrap_or(LUID {
|
||||
LowPart: (target.adapter_luid & 0xffff_ffff) as u32,
|
||||
HighPart: (target.adapter_luid >> 32) as i32,
|
||||
});
|
||||
match Self::open_on(target.clone(), preferred, client_10bit, luid) {
|
||||
Ok(me) => Ok(me),
|
||||
Err(e) => {
|
||||
// Self-heal a render-adapter mismatch ONCE: on TEX_FAIL the driver has reported the
|
||||
// adapter its swap-chain ACTUALLY renders on (a stale monitor across an adapter
|
||||
// re-init, or a driver that ignored SET_RENDER_ADAPTER). Rebinding the ring to that
|
||||
// adapter beats failing the session — the outer pipeline retries would repeat the
|
||||
// exact same mismatch.
|
||||
let driver_luid = e
|
||||
.downcast_ref::<AttachTexFail>()
|
||||
.map(|tf| tf.driver_luid)
|
||||
.filter(|d| *d != 0 && *d != crate::capture::dxgi::pack_luid(luid));
|
||||
let Some(packed) = driver_luid else {
|
||||
return Err(e);
|
||||
};
|
||||
let drv = LUID {
|
||||
LowPart: (packed & 0xffff_ffff) as u32,
|
||||
HighPart: (packed >> 32) as i32,
|
||||
};
|
||||
tracing::warn!(
|
||||
ring_adapter = format!("{:08x}:{:08x}", luid.HighPart, luid.LowPart),
|
||||
driver_adapter = format!("{:08x}:{:08x}", drv.HighPart, drv.LowPart),
|
||||
"IDD push: ring/driver render-adapter mismatch — rebinding the ring to the \
|
||||
driver's reported adapter"
|
||||
);
|
||||
Self::open_on(target, preferred, client_10bit, drv)
|
||||
.context("IDD-push rebind to the driver's reported render adapter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_on(
|
||||
target: WinCaptureTarget,
|
||||
preferred: Option<(u32, u32, u32)>,
|
||||
client_10bit: bool,
|
||||
luid: LUID,
|
||||
) -> Result<Self> {
|
||||
let (pw, ph, _hz) = preferred
|
||||
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
||||
@@ -829,10 +879,10 @@ impl IddPushCapturer {
|
||||
} else {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM
|
||||
};
|
||||
// Create our device on the discrete render GPU (where NVENC runs); the driver must render
|
||||
// the swap-chain on the SAME adapter for the shared textures to open (it reports its actual
|
||||
// render LUID into the header so we can detect a mismatch).
|
||||
let luid = resolve_render_adapter_luid_or(target.adapter_luid);
|
||||
// Our device (ring + zero-copy NVENC) lives on `luid` — the selected render GPU per
|
||||
// `open_inner`; the driver must render the swap-chain on the SAME adapter for the
|
||||
// shared textures to open (it reports its actual render LUID into the header, which
|
||||
// `open_inner` uses to rebind once if this mismatches).
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
||||
let adapter: IDXGIAdapter1 = factory
|
||||
.EnumAdapterByLuid(luid)
|
||||
@@ -991,13 +1041,31 @@ impl IddPushCapturer {
|
||||
// diagnostics — the real handshake is the atomic `magic`/`latest` (same access as
|
||||
// log_driver_status_once).
|
||||
let st = unsafe { (*self.header).driver_status };
|
||||
if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) {
|
||||
if st == DRV_STATUS_TEX_FAIL {
|
||||
// SAFETY: as above — in-bounds, aligned word reads of best-effort diagnostic fields
|
||||
// through the owned, live header mapping; no reference into the shared region is formed.
|
||||
// The driver wrote its render LUID BEFORE attempting the texture opens
|
||||
// (frame_transport.rs step 2), so it is valid here.
|
||||
let (detail, lo, hi) = unsafe {
|
||||
(
|
||||
(*self.header).driver_status_detail,
|
||||
(*self.header).driver_render_luid_low,
|
||||
(*self.header).driver_render_luid_high,
|
||||
)
|
||||
};
|
||||
// Typed so `open_inner` can rebind the ring to the driver's adapter once.
|
||||
return Err(anyhow::Error::new(AttachTexFail {
|
||||
detail,
|
||||
driver_luid: ((hi as i64) << 32) | (lo as i64 & 0xffff_ffff),
|
||||
}));
|
||||
}
|
||||
if st == DRV_STATUS_NO_DEVICE1 {
|
||||
// SAFETY: as above — an in-bounds, aligned `u32` read of a best-effort diagnostic field
|
||||
// through the owned, live header mapping; no reference into the shared region is formed.
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
bail!(
|
||||
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
|
||||
render-adapter mismatch?)"
|
||||
the driver has no ID3D11Device1 to open shared resources)"
|
||||
);
|
||||
}
|
||||
// Attached AND a frame has been published — the publish token's seq advances past 0.
|
||||
@@ -1415,17 +1483,31 @@ impl IddPushCapturer {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected render GPU LUID (where the encoder runs), falling back to the monitor's `OsAdapterLuid`.
|
||||
fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID {
|
||||
if let Some(l) = crate::win_adapter::resolve_render_adapter_luid() {
|
||||
return l;
|
||||
/// `wait_for_attach`'s DRV_STATUS_TEX_FAIL as a typed error: the driver could not open the ring
|
||||
/// textures, and `driver_luid` (packed, from the shared header) is the adapter its swap-chain
|
||||
/// ACTUALLY renders on — `open_inner` downcasts to this to rebind the ring there once.
|
||||
#[derive(Debug)]
|
||||
struct AttachTexFail {
|
||||
detail: u32,
|
||||
driver_luid: i64,
|
||||
}
|
||||
LUID {
|
||||
LowPart: (fallback_packed & 0xffff_ffff) as u32,
|
||||
HighPart: (fallback_packed >> 32) as i32,
|
||||
|
||||
impl std::fmt::Display for AttachTexFail {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"IDD-push driver failed to attach (driver_status={DRV_STATUS_TEX_FAIL} \
|
||||
detail=0x{:08x}): it could not open the ring textures — its swap-chain renders on \
|
||||
adapter {:08x}:{:08x}, not the ring's (render-adapter mismatch)",
|
||||
self.detail,
|
||||
(self.driver_luid >> 32) as i32,
|
||||
(self.driver_luid & 0xffff_ffff) as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AttachTexFail {}
|
||||
|
||||
impl Capturer for IddPushCapturer {
|
||||
fn next_frame(&mut self) -> Result<CapturedFrame> {
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
|
||||
@@ -91,7 +91,8 @@ impl GpuInfo {
|
||||
}
|
||||
|
||||
/// Assign the stable `id` + `occurrence` fields after enumeration (occurrence = index among
|
||||
/// same-(vendor,device) twins, in enumeration order).
|
||||
/// same-(vendor,device) twins, in inventory order — Windows sorts the inventory by LUID first so
|
||||
/// twin numbering is stable for the boot, see [`enumerate`]).
|
||||
fn assign_ids(gpus: &mut [GpuInfo]) {
|
||||
for i in 0..gpus.len() {
|
||||
let occ = gpus[..i]
|
||||
@@ -268,6 +269,13 @@ pub(crate) fn enumerate() -> Vec<GpuInfo> {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Deterministic inventory order. DXGI enumerates the adapter owning the primary display first,
|
||||
// and the vdisplay flow itself MOVES the primary mid-session (exclusive mode turns the physical
|
||||
// screens off) — which flipped every order-relative selection between IDENTICAL twin GPUs from
|
||||
// one call to the next (the max-VRAM tie, the env-substring first match, occurrence numbering):
|
||||
// monitor ADD pinned the driver to one twin, the ring open picked the other → TEX_FAIL. LUIDs
|
||||
// are creation-ordered and stable for the boot, so sorting by them makes selection stable too.
|
||||
out.sort_by_key(|g| ((g.handle.luid_high as i64) << 32) | g.handle.luid_low as i64);
|
||||
assign_ids(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user