feat(windows-host): IDD-push first-frame failover to DDA (game-capture bug GB1 pt1)

wait_for_attach now requires the driver to publish a FIRST frame, not just attach (DRV_STATUS_OPENED). A fullscreen game can leave the virtual display in a format/size the driver's publish() guard rejects -> the driver ATTACHES but silently drops every frame; previously the host sailed past open() and only died on next_frame's 20s deadline (the 'reconnect = black + working audio' symptom). Now open() fails -> capture.rs falls back to DDA (reusing the C1 fallback) -> the game is captured + visible after a reconnect.

Safe at open: the OS composites the freshly-activated virtual display, so a frame arrives within ~1s — a normal/idle open isn't false-failed; only a genuinely-broken display (no frame in 4s) falls back (and DDA is a working path, so even a false-positive degrades gracefully).

GB1 Stage 1a (docs/windows-host-rewrite-game-capture-bug.md P3). The mid-session-without-reconnect live failover (composing capturer) is the next piece.

Verified: host clippy (nvenc) clean on the RTX box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 14:50:12 +00:00
parent dbab1f98ba
commit f98ab07dd6
+28 -18
View File
@@ -507,37 +507,47 @@ impl IddPushCapturer {
// it back to the caller for the DDA fallback (audit §5.1). // it back to the caller for the DDA fallback (audit §5.1).
_keepalive: Box::new(()), _keepalive: Box::new(()),
}; };
// Bounded wait for the driver to ATTACH to the ring (it writes DRV_STATUS_OPENED). An attach // Bounded wait for the driver to ATTACH to the ring AND publish a first frame. An attach
// failure (e.g. the OS rendered the IDD on a different GPU than our ring → DRV_STATUS_TEX_FAIL) // failure (DRV_STATUS_TEX_FAIL) or an attach-but-no-frames (a game left the display in a
// becomes an open failure the caller falls back from, instead of next_frame's 20 s deadline. // format/size the ring can't match) becomes an open failure the caller falls back from (→ DDA),
// instead of next_frame's 20 s black-then-bail.
me.wait_for_attach()?; me.wait_for_attach()?;
Ok(me) Ok(me)
} }
} }
/// Block (bounded) until the driver attaches to the host ring, else fail so the caller can fall back /// Block (bounded) until the driver has ATTACHED to the host ring (`DRV_STATUS_OPENED`) **and published
/// to DDA (audit §5.1). Checks `driver_status` (NOT frame arrival — an idle desktop may present no /// a first frame**, else fail so the caller can fall back to DDA (audit §5.1 +
/// frame yet), so it never falsely fails on the happy path: the driver writes `DRV_STATUS_OPENED` as /// `docs/windows-host-rewrite-game-capture-bug.md` P3/Stage 1).
/// soon as it opens the ring textures, regardless of whether DWM has composed a frame. ///
/// Requiring the first frame — not just the attach — catches the *reconnect-into-a-broken-state* case:
/// a fullscreen game can leave the virtual display in a format/size that the driver's `publish()` guard
/// rejects, so the driver ATTACHES but silently drops every frame; without this the host sails past
/// `open()` and only dies on `next_frame`'s 20 s deadline (the "reconnect = black + audio" symptom). At
/// session open the OS activates the virtual display → DWM composites it → a frame arrives within ~1 s,
/// so this does not false-fail a normal (even idle) open; no frame within the window = genuinely broken.
fn wait_for_attach(&self) -> Result<()> { fn wait_for_attach(&self) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(4); let deadline = Instant::now() + Duration::from_secs(4);
loop { loop {
// Plain read: the driver writes this u32; an aligned u32 read can't tear (same access as // Plain read: the driver writes this u32; an aligned u32 read can't tear (same access as
// log_driver_status_once). // log_driver_status_once).
let st = unsafe { (*self.header).driver_status }; let st = unsafe { (*self.header).driver_status };
match st { if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) {
DRV_STATUS_OPENED => return Ok(()), let detail = unsafe { (*self.header).driver_status_detail };
DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1 => { bail!(
let detail = unsafe { (*self.header).driver_status_detail }; "IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \
bail!( render-adapter mismatch?)"
"IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \ );
render-adapter mismatch?)" }
); // Attached AND a frame has been published — the publish token's seq advances past 0.
} if st == DRV_STATUS_OPENED && frame::FrameToken::unpack(self.latest()).seq != 0 {
_ => {} return Ok(());
} }
if Instant::now() > deadline { if Instant::now() > deadline {
bail!("IDD-push driver did not attach within 4s (driver_status={st})"); bail!(
"IDD-push: driver_status={st} but no frame published within 4s — the virtual display \
is likely in a format/size the ring can't match (fullscreen game?); falling back"
);
} }
std::thread::sleep(Duration::from_millis(20)); std::thread::sleep(Duration::from_millis(20));
} }