feat(host/windows): HID compose kick — wake and dirty the display through the virtual mouse (lid-closed first-frame fix)
The final piece of the lid-closed field report (fixes 1-3: 3d9b3290; prerequisites:85dd2bb0pf-mouse,845a9760leaf primitives,94ca4041pf-capture hook): the IDD first-frame gate can only pass if DWM composes at least one frame, and a lid-closed/locked/idle machine is exactly the state Windows has decided not to compose in. The SendInput compose kick is conditional on this process's context — wrong session → wrong input queue; secure desktop → blocked; display powered off → no wake. A report from the resident pf-mouse HID device is REAL input to win32k: session-independent, secure-desktop-proof, wakes a powered-off display, counts as user presence. Parsec-class mechanism, and semantically honest — a remote user starting a stream IS a user arriving at this machine. - hid_kick(rect, bounds): newest-wins kick slot + condvar, serviced by the keeper thread that owns the ONE process-wide VirtualMouse (a second open() would squat the bootstrap mailbox). Not-ready (opt-out, driver missing, not yet attached) returns false → the capture crate falls back to SendInput. - perform_kick: park the pointer at the target display's center, dwell 35 ms (Stage-W3: DWM samples cursor position at the next vsync tick; the gaps also keep reports from coalescing in the driver's 8 ms timer), wiggle ~2 px, restore the saved position. Desktop→HID coordinates normalize against pf_win_display::desktop_bounds() (CCD union — correct from any session, unlike per-session GDI metrics). - ensure_resident registers the hook (pf_capture::HID_COMPOSE_KICK) — the one-way-edge inversion: pf-capture never reaches into inject. - keeper loop: condvar wait (250 ms tick) so a kick executes immediately, not at the next tick; publishes MOUSE_READY from driver_proto. Paired with pf-frame's DisplayWakeRequest (held by the capturer from before the first-frame gate): the power request keeps the display from going dark mid-session, the HID kick wakes it when it already is. Verified on winbox: combined-tree cargo check + clippy for punktfunk-host, pf-capture, pf-frame, pf-win-display all EXIT 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,11 @@ use super::dualsense_windows::{create_swdevice, SwDeviceProfile};
|
||||
use super::gamepad_raii::{DriverAttach, PadChannel};
|
||||
use anyhow::Result;
|
||||
use pf_driver_proto::mouse::{input_report, mouse_boot_name, MouseShm, MOUSE_MAGIC};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Condvar, Mutex};
|
||||
use std::time::Duration;
|
||||
use windows::Win32::Foundation::POINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
||||
|
||||
const SHM_SIZE: usize = core::mem::size_of::<MouseShm>();
|
||||
const OFF_IN_SEQ: usize = core::mem::offset_of!(MouseShm, in_seq);
|
||||
@@ -128,6 +131,96 @@ impl VirtualMouse {
|
||||
}
|
||||
}
|
||||
|
||||
/// One pending compose-kick aim, desktop coordinates: the target display's rect plus the
|
||||
/// virtual-desktop bounds to normalize against (both from CCD, so they describe the CONSOLE's
|
||||
/// layout whatever session this process is in). Newest-wins single slot — kicks are idempotent
|
||||
/// damage nudges, queueing them would only multiply pointer blips.
|
||||
struct KickAim {
|
||||
rect: (i32, i32, i32, i32),
|
||||
bounds: (i32, i32, i32, i32),
|
||||
}
|
||||
|
||||
struct KickSlot {
|
||||
slot: Mutex<Option<KickAim>>,
|
||||
wake: Condvar,
|
||||
}
|
||||
|
||||
static KICK: KickSlot = KickSlot {
|
||||
slot: Mutex::new(None),
|
||||
wake: Condvar::new(),
|
||||
};
|
||||
|
||||
/// True while the keeper's mouse is open AND the pf-mouse driver is attached (its 8 ms timer
|
||||
/// stamps `driver_proto`) — the only state in which a kick's reports actually reach win32k.
|
||||
static MOUSE_READY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Request a pointer jiggle on the given display through the resident virtual mouse — the
|
||||
/// COMPOSE KICK's reliable arm. A report from a HID device is REAL input to win32k: it wakes a
|
||||
/// powered-off display subsystem (lid-closed / display idle-off / modern standby), resets idle
|
||||
/// timers, counts as user presence, and is delivered regardless of the calling process's session
|
||||
/// or the active desktop — every condition under which the `SendInput` kick is silently impotent
|
||||
/// (wrong session → wrong input queue; secure desktop → blocked; display-off → nothing to
|
||||
/// damage). Asynchronous: the keeper thread (which owns the one process-wide mouse) executes it
|
||||
/// within its tick. Returns `false` when the resident mouse isn't up (opted out, driver not
|
||||
/// installed, not yet attached) — the caller falls back to `SendInput`.
|
||||
pub(crate) fn hid_kick(rect: (i32, i32, i32, i32), bounds: (i32, i32, i32, i32)) -> bool {
|
||||
if !MOUSE_READY.load(Ordering::Relaxed) {
|
||||
return false;
|
||||
}
|
||||
*KICK.slot.lock().unwrap() = Some(KickAim { rect, bounds });
|
||||
KICK.wake.notify_one();
|
||||
true
|
||||
}
|
||||
|
||||
/// Execute one compose kick on the keeper thread: park the pointer at the target rect's center,
|
||||
/// dwell one composition interval, wiggle ~2 px, then put it back where it was. Every report is
|
||||
/// device-level input (see [`hid_kick`]). The dwell is load-bearing (the Stage-W3 on-glass
|
||||
/// finding, same as the SendInput jump path): DWM samples the cursor position at the next vsync
|
||||
/// tick, so a sub-tick round trip composes nothing. The gaps also respect the driver's 8 ms
|
||||
/// report timer — back-to-back writes into the single report slot would coalesce.
|
||||
///
|
||||
/// The restore is best-effort via `GetCursorPos`: in a wrong-session host it describes the wrong
|
||||
/// session's pointer, so the console pointer is instead left near the target's center — which is
|
||||
/// the streamed display, exactly where the pointer is about to be useful.
|
||||
fn perform_kick(m: &mut VirtualMouse, aim: KickAim) {
|
||||
let (bx, by, bw, bh) = aim.bounds;
|
||||
if bw <= 0 || bh <= 0 {
|
||||
return;
|
||||
}
|
||||
// Field-log which kick arm fired (the SendInput arm logs in kick_dwm_compose) — a lid-closed
|
||||
// repro should show this line followed by the driver's first acquired frame.
|
||||
tracing::debug!(
|
||||
rect = ?aim.rect,
|
||||
bounds = ?aim.bounds,
|
||||
"HID compose kick — parking the pointer on the target display (display wake + damage)"
|
||||
);
|
||||
let map = |px: i32, py: i32| -> (u16, u16) {
|
||||
let nx = ((px - bx).clamp(0, bw - 1) as i64 * 0x7FFF) / i64::from(bw - 1).max(1);
|
||||
let ny = ((py - by).clamp(0, bh - 1) as i64 * 0x7FFF) / i64::from(bh - 1).max(1);
|
||||
(nx as u16, ny as u16)
|
||||
};
|
||||
let mut p = POINT::default();
|
||||
// SAFETY: plain FFI; `p` is a valid out-param for this synchronous call.
|
||||
let orig = unsafe { GetCursorPos(&mut p) }
|
||||
.is_ok()
|
||||
.then_some((p.x, p.y));
|
||||
let (rx, ry, rw, rh) = aim.rect;
|
||||
let (cx, cy) = map(rx + rw / 2, ry + rh / 2);
|
||||
// ~2 desktop pixels in HID units, at least 1 — the wiggle must actually move the pointer.
|
||||
let dx = ((2 * 0x7FFF) / bw.max(1)).max(1) as u16;
|
||||
m.send_report(0, cx, cy, 0, 0);
|
||||
std::thread::sleep(Duration::from_millis(35));
|
||||
m.send_report(0, cx.saturating_add(dx).min(0x7FFF), cy, 0, 0);
|
||||
std::thread::sleep(Duration::from_millis(35));
|
||||
match orig {
|
||||
Some((ox, oy)) => {
|
||||
let (ox, oy) = map(ox, oy);
|
||||
m.send_report(0, ox, oy, 0, 0);
|
||||
}
|
||||
None => m.send_report(0, cx, cy, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure the resident virtual mouse exists (idempotent, best-effort). Called whenever an
|
||||
/// [`InjectorService`](crate::inject::InjectorService) starts — multiple services (native +
|
||||
/// GameStream) share the ONE process-wide mouse, guarded here. Spawns a keeper thread that owns
|
||||
@@ -147,6 +240,11 @@ pub(crate) fn ensure_resident() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Hand the capture crate its HID compose-kick hook (the one-way-edge inversion: pf-capture
|
||||
// never reaches back into inject). Registered exactly when the resident mouse is being
|
||||
// brought up; until the driver actually attaches, `hid_kick` reports not-ready and the
|
||||
// kick falls back to SendInput.
|
||||
let _ = pf_capture::HID_COMPOSE_KICK.set(hid_kick);
|
||||
if let Err(e) = std::thread::Builder::new()
|
||||
.name("punktfunk-vmouse".into())
|
||||
.spawn(keeper_thread)
|
||||
@@ -159,6 +257,9 @@ pub(crate) fn ensure_resident() {
|
||||
/// Open-with-retry, then hold + pump forever. Open only realistically fails on a mailbox squat
|
||||
/// (another punktfunk-host instance) — retry slowly; a missing/failed DRIVER is not an open
|
||||
/// failure (the devnode exists but nothing binds), which [`DriverAttach`] diagnoses via the pump.
|
||||
/// Each tick also publishes kick-readiness ([`MOUSE_READY`]) and executes at most one pending
|
||||
/// compose kick ([`hid_kick`]) — the condvar wait keeps kick latency at "immediately", not "next
|
||||
/// 250 ms tick", while an idle keeper still only wakes 4×/s.
|
||||
fn keeper_thread() {
|
||||
loop {
|
||||
match VirtualMouse::open() {
|
||||
@@ -169,7 +270,22 @@ fn keeper_thread() {
|
||||
);
|
||||
loop {
|
||||
m.service();
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
MOUSE_READY.store(m.driver_proto() != 0, Ordering::Relaxed);
|
||||
let (mut slot, _timeout) = KICK
|
||||
.wake
|
||||
.wait_timeout_while(
|
||||
KICK.slot.lock().unwrap(),
|
||||
Duration::from_millis(250),
|
||||
|k| k.is_none(),
|
||||
)
|
||||
.unwrap();
|
||||
let aim = slot.take();
|
||||
drop(slot);
|
||||
if let Some(aim) = aim {
|
||||
if m.driver_proto() != 0 {
|
||||
perform_kick(&mut m, aim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user