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:
@@ -351,9 +351,21 @@ pub fn capture_virtual_output(
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// overlay/independent-flip planes DXGI Desktop Duplication misses (the frozen-HDR-animation bug),
|
||||
// and has no ACCESS_LOST-on-overlay churn. DDA stays available via PUNKTFUNK_CAPTURE=dda and is
|
||||
|
||||
@@ -208,7 +208,12 @@ pub fn open_or_reuse(
|
||||
client_10bit,
|
||||
"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))
|
||||
@@ -331,11 +336,29 @@ impl IddPushCapturer {
|
||||
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(
|
||||
target: WinCaptureTarget,
|
||||
preferred: Option<(u32, u32, u32)>,
|
||||
client_10bit: bool,
|
||||
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> {
|
||||
let (w, h, _hz) = preferred
|
||||
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
||||
@@ -451,7 +474,7 @@ impl IddPushCapturer {
|
||||
ring_fp16 = display_hdr,
|
||||
"IDD push(host): created shared ring; waiting for the driver to attach + publish"
|
||||
);
|
||||
Ok(Self {
|
||||
let me = Self {
|
||||
device,
|
||||
context,
|
||||
target_id: target.target_id,
|
||||
@@ -474,8 +497,43 @@ impl IddPushCapturer {
|
||||
last_present: None,
|
||||
status_logged: false,
|
||||
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();
|
||||
}
|
||||
Err(e) => tracing::warn!(
|
||||
Err((e, _keep)) => tracing::warn!(
|
||||
target_id = tid,
|
||||
"IDD push OBSERVER: ring open failed: {e:#}"
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user