feat(windows-host): IDD-push attach fallback to DDA, not the 20s black bail (audit §5.1)

open() now hands the keepalive BACK on failure (the WGC attach_keepalive pattern) so the caller can fall back instead of tearing the virtual display down. Added a bounded wait_for_attach() that polls the driver's DRV_STATUS_OPENED — it checks ATTACH status, not frame arrival, so it never false-fails on an idle desktop that has composed no frame yet.

An attach failure (e.g. a hybrid-GPU render mismatch -> DRV_STATUS_TEX_FAIL, or the driver never opening the ring within 4s) now fails open() -> capture.rs falls back to DDA, instead of next_frame's 20s deadline leaving the session black. Pairs with the driver SET_RENDER_ADAPTER fix (0a7ae5e).

Verified: host clippy (nvenc) clean on the RTX box. Behavioral validation (fallback trigger + happy-path attach timing) needs an on-glass session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 13:09:28 +00:00
parent e5c9ee8327
commit ed583650a6
2 changed files with 77 additions and 7 deletions
+14 -2
View File
@@ -351,8 +351,20 @@ pub fn capture_virtual_output(
// the host can't revive it. The driver's recreate crash (target id resolved to 0) is fixed by // the host can't revive it. The driver's recreate crash (target id resolved to 0) is fixed by
// stamping target_id onto the monitor context. The ring is always FP16 (the driver composes // stamping target_id onto the monitor context. The ring is always FP16 (the driver composes
// the IDD in FP16); `want_hdr` selects the per-frame conversion (FP16 → Rgb10a2 vs Bgra). // the IDD in FP16); `want_hdr` selects the per-frame conversion (FP16 → Rgb10a2 vs Bgra).
return idd_push::IddPushCapturer::open(target, pref, want_hdr, keep) // If IDD-push can't open OR the driver doesn't attach to the ring within a few seconds (e.g. a
.map(|c| Box::new(c) as Box<dyn Capturer>); // hybrid-GPU render mismatch), fall back to DDA so the session is NEVER left black (audit §5.1).
// `open()` hands the keepalive back on failure so DDA can take ownership of the virtual display.
match idd_push::IddPushCapturer::open(target.clone(), pref, want_hdr, keep) {
Ok(c) => return Ok(Box::new(c) as Box<dyn Capturer>),
Err((e, keep)) => {
tracing::warn!(
error = %format!("{e:#}"),
"IDD-push open/attach failed — falling back to DDA"
);
return dxgi::DuplCapturer::open(target, pref, keep, false)
.map(|c| Box::new(c) as Box<dyn Capturer>);
}
}
} }
// WGC (Windows.Graphics.Capture) is the default: it captures the COMPOSED desktop including the // WGC (Windows.Graphics.Capture) is the default: it captures the COMPOSED desktop including the
// overlay/independent-flip planes DXGI Desktop Duplication misses (the frozen-HDR-animation bug), // overlay/independent-flip planes DXGI Desktop Duplication misses (the frozen-HDR-animation bug),
+63 -5
View File
@@ -208,7 +208,12 @@ pub fn open_or_reuse(
client_10bit, client_10bit,
"IDD push: creating the persistent capturer (first session)" "IDD push: creating the persistent capturer (first session)"
); );
*slot = Some(IddPushCapturer::open(target, preferred, client_10bit, keepalive)?); // (dead persistent path) open() now returns the keepalive on failure; this path has no
// fallback, so discard it on error.
*slot = Some(
IddPushCapturer::open(target, preferred, client_10bit, keepalive)
.map_err(|(e, _keepalive)| e)?,
);
} }
} }
Ok(Box::new(IddReuseHandle)) Ok(Box::new(IddReuseHandle))
@@ -331,11 +336,29 @@ impl IddPushCapturer {
Ok(slots) Ok(slots)
} }
/// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the
/// virtual display); on FAILURE the keepalive is handed BACK so the caller can fall back to DDA
/// instead of tearing the display down (audit §5.1 — no more 20 s black bail). "Failure" includes the
/// driver not attaching to the ring within a few seconds (e.g. a hybrid-GPU render mismatch).
pub fn open( pub fn open(
target: WinCaptureTarget, target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>, preferred: Option<(u32, u32, u32)>,
client_10bit: bool, client_10bit: bool,
keepalive: Box<dyn Send>, keepalive: Box<dyn Send>,
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
match Self::open_inner(target, preferred, client_10bit) {
Ok(mut me) => {
me._keepalive = keepalive;
Ok(me)
}
Err(e) => Err((e, keepalive)),
}
}
fn open_inner(
target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>,
client_10bit: bool,
) -> Result<Self> { ) -> Result<Self> {
let (w, h, _hz) = preferred let (w, h, _hz) = preferred
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?; .context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
@@ -451,7 +474,7 @@ impl IddPushCapturer {
ring_fp16 = display_hdr, ring_fp16 = display_hdr,
"IDD push(host): created shared ring; waiting for the driver to attach + publish" "IDD push(host): created shared ring; waiting for the driver to attach + publish"
); );
Ok(Self { let me = Self {
device, device,
context, context,
target_id: target.target_id, target_id: target.target_id,
@@ -474,8 +497,43 @@ impl IddPushCapturer {
last_present: None, last_present: None,
status_logged: false, status_logged: false,
my_gen: crate::vdisplay::sudovda::CURRENT_MON_GEN.load(Ordering::Relaxed), my_gen: crate::vdisplay::sudovda::CURRENT_MON_GEN.load(Ordering::Relaxed),
_keepalive: keepalive, // Placeholder; `open()` attaches the real keepalive on success, so a FAILED open can hand
}) // it back to the caller for the DDA fallback (audit §5.1).
_keepalive: Box::new(()),
};
// Bounded wait for the driver to ATTACH to the ring (it writes DRV_STATUS_OPENED). An attach
// failure (e.g. the OS rendered the IDD on a different GPU than our ring → DRV_STATUS_TEX_FAIL)
// becomes an open failure the caller falls back from, instead of next_frame's 20 s deadline.
me.wait_for_attach()?;
Ok(me)
}
}
/// Block (bounded) until the driver attaches to the host ring, else fail so the caller can fall back
/// to DDA (audit §5.1). Checks `driver_status` (NOT frame arrival — an idle desktop may present no
/// frame yet), so it never falsely fails on the happy path: the driver writes `DRV_STATUS_OPENED` as
/// soon as it opens the ring textures, regardless of whether DWM has composed a frame.
fn wait_for_attach(&self) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(4);
loop {
// Plain read: the driver writes this u32; an aligned u32 read can't tear (same access as
// log_driver_status_once).
let st = unsafe { (*self.header).driver_status };
match st {
DRV_STATUS_OPENED => return Ok(()),
DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1 => {
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?)"
);
}
_ => {}
}
if Instant::now() > deadline {
bail!("IDD-push driver did not attach within 4s (driver_status={st})");
}
std::thread::sleep(Duration::from_millis(20));
} }
} }
@@ -796,7 +854,7 @@ pub fn spawn_observer(target: WinCaptureTarget, preferred: Option<(u32, u32, u32
); );
cap.log_debug_block(); cap.log_debug_block();
} }
Err(e) => tracing::warn!( Err((e, _keep)) => tracing::warn!(
target_id = tid, target_id = tid,
"IDD push OBSERVER: ring open failed: {e:#}" "IDD push OBSERVER: ring open failed: {e:#}"
), ),