From f98ab07dd60e699881ffbe97ea659a78729edefc Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 25 Jun 2026 14:50:12 +0000 Subject: [PATCH] feat(windows-host): IDD-push first-frame failover to DDA (game-capture bug GB1 pt1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/punktfunk-host/src/capture/idd_push.rs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/punktfunk-host/src/capture/idd_push.rs b/crates/punktfunk-host/src/capture/idd_push.rs index cbc3ed8..2ae9412 100644 --- a/crates/punktfunk-host/src/capture/idd_push.rs +++ b/crates/punktfunk-host/src/capture/idd_push.rs @@ -507,37 +507,47 @@ impl IddPushCapturer { // 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. + // Bounded wait for the driver to ATTACH to the ring AND publish a first frame. An attach + // failure (DRV_STATUS_TEX_FAIL) or an attach-but-no-frames (a game left the display in a + // 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()?; 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. + /// Block (bounded) until the driver has ATTACHED to the host ring (`DRV_STATUS_OPENED`) **and published + /// a first frame**, else fail so the caller can fall back to DDA (audit §5.1 + + /// `docs/windows-host-rewrite-game-capture-bug.md` P3/Stage 1). + /// + /// 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<()> { 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 matches!(st, 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?)" + ); + } + // 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 { - 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)); }