diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs
index c9b1448..5a91504 100644
--- a/crates/punktfunk-host/src/capture/windows/idd_push.rs
+++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs
@@ -55,6 +55,9 @@ use windows::Win32::System::Threading::{
CreateEventW, GetCurrentProcess, OpenProcess, QueryFullProcessImageNameW, WaitForSingleObject,
PROCESS_DUP_HANDLE, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE,
};
+use windows::Win32::UI::Input::KeyboardAndMouse::{
+ SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_MOVE, MOUSEINPUT,
+};
// The frame-transport contract — `SharedHeader` layout, `MAGIC`/`VERSION`/`RING_LEN`, the
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
@@ -188,6 +191,36 @@ impl Drop for KeyedMutexGuard<'_> {
}
}
+/// Nudge DWM into composing the virtual display: two net-zero 1 px relative mouse moves via
+/// `SendInput`. DWM presents a display only when something DIRTIES it — an idle desktop never does,
+/// so a freshly-attached ring (session open, or a mid-session ring recreate) can sit at E_PENDING
+/// with no first frame even though everything is healthy. pf-vdisplay implements no hardware-cursor
+/// plane, so a cursor move is composited into the frame — a guaranteed real present onto the IDD
+/// swap-chain (empirically what `punktfunk-probe --input-test` always relied on). Net-zero: the
+/// pointer ends exactly where it started; the 1 px round trip is imperceptible, and each event still
+/// dirties the cursor layer. Best-effort — injection can be unavailable on the secure desktop, where
+/// a fresh compose just happened anyway.
+fn kick_dwm_compose() {
+ let mk = |dx: i32| INPUT {
+ r#type: INPUT_MOUSE,
+ Anonymous: INPUT_0 {
+ mi: MOUSEINPUT {
+ dx,
+ dy: 0,
+ mouseData: 0,
+ dwFlags: MOUSEEVENTF_MOVE,
+ time: 0,
+ dwExtraInfo: 0,
+ },
+ },
+ };
+ // SAFETY: plain FFI; the input slice is valid, fully-initialized local data for this synchronous
+ // call, and `cbsize` is the true element size.
+ unsafe {
+ let _ = SendInput(&[mk(1), mk(-1)], std::mem::size_of::() as i32);
+ }
+}
+
/// Confirm the process is a genuine system WUDFHost — `%SystemRoot%\System32\WUDFHost.exe` — before a
/// broker duplicates sensitive handles into it. The pid is driver-reported (the frame channel's
/// [`control::AddReply::wudf_pid`], or the gamepad bootstrap's `driver_pid`); a spoofed devnode / a
@@ -460,6 +493,8 @@ pub struct IddPushCapturer {
last_fresh: Instant,
/// Rate-limits the WUDFHost liveness probe (one 0 ms wait per second, and only while stale).
last_liveness: Instant,
+ /// Rate-limits the mid-session [`kick_dwm_compose`] nudge (recovery window only).
+ last_kick: Instant,
/// Host-owned ROTATING output ring NVENC encodes (one YUV texture per slot). Rotating it per frame
/// is the precondition for pipelining the encode loop: while NVENC encodes frame N's texture on the
/// ASIC, frame N+1's convert writes a DIFFERENT texture — the two overlap. Format = `out_format()`:
@@ -778,6 +813,7 @@ impl IddPushCapturer {
recovering_since: None,
last_fresh: Instant::now(),
last_liveness: Instant::now(),
+ last_kick: Instant::now(),
out_ring: Vec::new(),
out_idx: 0,
video_conv: None,
@@ -810,6 +846,12 @@ impl IddPushCapturer {
/// 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);
+ // Compose-kick schedule: DWM only presents a display something DIRTIED, so on an idle
+ // desktop a perfectly healthy attach sees no first frame (E_PENDING forever) and this gate
+ // used to fail the session — the "idle desktop → no frames" gotcha (a real client escaped
+ // it only because its own input soon dirtied the desktop; a headless probe never did).
+ // Give the natural post-activate compose a moment, then nudge.
+ let mut next_kick = Instant::now() + Duration::from_millis(600);
loop {
// SAFETY: `self.header` points into the live shared-header mapping this capturer owns (sized
// `>= size_of::()`, page-aligned), so the field read is in-bounds + aligned, and
@@ -831,10 +873,15 @@ impl IddPushCapturer {
if st == DRV_STATUS_OPENED && frame::FrameToken::unpack(self.latest()).seq != 0 {
return Ok(());
}
+ if Instant::now() >= next_kick {
+ kick_dwm_compose();
+ next_kick = Instant::now() + Duration::from_millis(800);
+ }
if Instant::now() > deadline {
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"
+ "IDD-push: driver_status={st} but no frame published within 4s (despite compose \
+ kicks) — 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));
@@ -1098,6 +1145,16 @@ impl IddPushCapturer {
dropping the session so the client reconnects"
);
}
+ // Same idle-desktop stall as the open-time attach gate: after a mid-session ring
+ // recreate (HDR flip / mode change) an idle desktop composes nothing, so the fresh ring
+ // never sees a frame and the 3 s recover-or-drop above kills a healthy session. Nudge
+ // DWM (rate-limited) once the natural post-recreate compose has had its chance.
+ if since.elapsed() > Duration::from_millis(600)
+ && self.last_kick.elapsed() > Duration::from_millis(800)
+ {
+ self.last_kick = Instant::now();
+ kick_dwm_compose();
+ }
}
// Driver-death watch (the SDR path has no other signal): a dead WUDFHost stops publishing,
// which at the ring is indistinguishable from an idle desktop — the encode loop would repeat