From 845a97601d10281616d4477253125ff97c08dce8 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 17 Jul 2026 11:17:29 +0200 Subject: [PATCH] feat(pf-frame,pf-win-display): leaf primitives for the lid-closed first-frame fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three leaf-crate additions the IDD-push capturer (pf-capture, plan §W6 C6) builds on — committed ahead so the capture-crate extraction and the HID compose kick can land on top: - pf-frame session_tuning::DisplayWakeRequest — RAII PowerCreateRequest/ PowerSetRequest(PowerRequestDisplayRequired + SystemRequired), the service-grade 'someone is watching this screen' assertion (visible in powercfg /requests), held for a capture session so the console cannot drop into display-off mid-stream. Object-lifetime, unlike the thread-bound ES_* flags in on_hot_thread. Prevention only: no power request turns an already-off display back on — that wake is input's job (the virtual-mouse compose kick). - pf-win-display win_display::desktop_bounds() — the virtual-desktop bounds as the union of every ACTIVE CCD path's source rect. From the CCD database (global), NOT GetSystemMetrics (a per-session view), so a non-console-session host still aims HID absolute coordinates at the console's real layout. - pf-win-display console_session_mismatch() — the session guard from 3d9b3290, copied into the leaf so pf-capture reads it as a peer instead of reaching into the orchestrator (relocation authored by the W6 extraction session). Co-Authored-By: Claude Fable 5 --- crates/pf-frame/src/session_tuning.rs | 72 +++++++++++++++++++++++- crates/pf-win-display/Cargo.toml | 3 + crates/pf-win-display/src/lib.rs | 22 ++++++++ crates/pf-win-display/src/win_display.rs | 30 ++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/crates/pf-frame/src/session_tuning.rs b/crates/pf-frame/src/session_tuning.rs index c23f0820..0e6700a6 100644 --- a/crates/pf-frame/src/session_tuning.rs +++ b/crates/pf-frame/src/session_tuning.rs @@ -32,6 +32,21 @@ mod imp { fn GetCurrentProcess() -> Handle; fn SetPriorityClass(hProcess: Handle, dwPriorityClass: u32) -> Bool; fn SetThreadExecutionState(esFlags: u32) -> u32; + fn PowerCreateRequest(Context: *const ReasonContext) -> Handle; + fn PowerSetRequest(PowerRequest: Handle, RequestType: i32) -> Bool; + fn PowerClearRequest(PowerRequest: Handle, RequestType: i32) -> Bool; + fn CloseHandle(hObject: Handle) -> Bool; + } + + /// `REASON_CONTEXT` (minwinbase.h), simple-string flavour: `Version` (ULONG), `Flags` (DWORD), + /// then the union collapses to `SimpleReasonString` (LPWSTR) under + /// `POWER_REQUEST_CONTEXT_SIMPLE_STRING` — same size/alignment as the C layout (4+4, 8-aligned + /// pointer). + #[repr(C)] + struct ReasonContext { + version: u32, + flags: u32, + simple_reason: *const u16, } #[link(name = "dwmapi")] extern "system" { @@ -46,6 +61,61 @@ mod imp { const ES_CONTINUOUS: u32 = 0x8000_0000; const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001; const ES_DISPLAY_REQUIRED: u32 = 0x0000_0002; + const POWER_REQUEST_CONTEXT_VERSION: u32 = 0; // DIAGNOSTIC_REASON_VERSION + const POWER_REQUEST_CONTEXT_SIMPLE_STRING: u32 = 0x0000_0001; + const POWER_REQUEST_DISPLAY_REQUIRED: i32 = 0; + const POWER_REQUEST_SYSTEM_REQUIRED: i32 = 1; + const INVALID_HANDLE_VALUE: isize = -1; + + /// RAII display+system availability request (`PowerRequestDisplayRequired`, visible in + /// `powercfg /requests`) — the service-grade "someone is watching this screen" assertion, + /// held for a capture session so the console cannot drop into display-off mid-stream. This is + /// object-lifetime (unlike the thread-bound `ES_*` flags in [`on_hot_thread`], which the OS + /// reverts at thread exit), so a capturer can hold it across whatever threads serve the + /// session. PREVENTION only: no power request turns an already-off display back ON — that + /// wake needs input, which is the virtual-mouse compose kick's job. + pub struct DisplayWakeRequest(Handle); + + // SAFETY: the wrapped power-request HANDLE is a kernel object handle — a plain opaque value + // that any thread may use; this type never aliases it (set at new, cleared+closed at drop). + unsafe impl Send for DisplayWakeRequest {} + + impl DisplayWakeRequest { + /// Create + set the request. `None` when the kernel refuses (best-effort — the caller + /// streams without the assertion, exactly the pre-existing behavior). + pub fn new() -> Option { + let reason: Vec = "punktfunk streaming session\0".encode_utf16().collect(); + let ctx = ReasonContext { + version: POWER_REQUEST_CONTEXT_VERSION, + flags: POWER_REQUEST_CONTEXT_SIMPLE_STRING, + simple_reason: reason.as_ptr(), + }; + // SAFETY: `ctx` (and the reason buffer it points into) outlives the call, which copies + // the string into the kernel object; the returned handle is owned here and released in + // Drop. PowerSetRequest takes the just-created handle + a plain enum value. + unsafe { + let h = PowerCreateRequest(&ctx); + if h.is_null() || h as isize == INVALID_HANDLE_VALUE { + return None; + } + PowerSetRequest(h, POWER_REQUEST_DISPLAY_REQUIRED); + PowerSetRequest(h, POWER_REQUEST_SYSTEM_REQUIRED); + Some(DisplayWakeRequest(h)) + } + } + } + + impl Drop for DisplayWakeRequest { + fn drop(&mut self) { + // SAFETY: `self.0` is the owned, still-open power-request handle (created in `new`, + // dropped exactly once); clear + close are plain handle calls. + unsafe { + PowerClearRequest(self.0, POWER_REQUEST_DISPLAY_REQUIRED); + PowerClearRequest(self.0, POWER_REQUEST_SYSTEM_REQUIRED); + CloseHandle(self.0); + } + } + } static PROCESS_TUNED: OnceLock<()> = OnceLock::new(); @@ -94,7 +164,7 @@ mod imp { } #[cfg(target_os = "windows")] -pub use imp::on_hot_thread; +pub use imp::{on_hot_thread, DisplayWakeRequest}; /// No-op on non-Windows (Linux uses `setpriority` nice + CUDA stream priority instead — see /// `native::boost_thread_priority` and `zerocopy::cuda`). diff --git a/crates/pf-win-display/Cargo.toml b/crates/pf-win-display/Cargo.toml index 596d01d2..6bec47f6 100644 --- a/crates/pf-win-display/Cargo.toml +++ b/crates/pf-win-display/Cargo.toml @@ -29,4 +29,7 @@ windows = { version = "0.62", features = [ "Win32_Graphics_Gdi", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", + # console_session_mismatch: WTSGetActiveConsoleSessionId + ProcessIdToSessionId + GetCurrentProcessId. + "Win32_System_RemoteDesktop", + "Win32_System_Threading", ] } diff --git a/crates/pf-win-display/src/lib.rs b/crates/pf-win-display/src/lib.rs index 57fbe565..8861f6d8 100644 --- a/crates/pf-win-display/src/lib.rs +++ b/crates/pf-win-display/src/lib.rs @@ -15,3 +15,25 @@ pub mod display_events; pub mod monitor_devnode; #[cfg(target_os = "windows")] pub mod win_display; + +/// `Some((own_session, console_session))` when this process is NOT in the active console session — +/// the state where every `SetDisplayConfig`/CDS write fails `ERROR_ACCESS_DENIED`, GDI reads +/// describe the wrong session's displays, and input compose kicks go nowhere (the lid-closed / +/// non-console field failure mode). The IDD-push capturer uses it to phrase a diagnostic when the +/// driver won't attach. (A byte-copy of the host's `interactive::console_session_mismatch`: it lives +/// here too so `pf-capture` gets it as a leaf peer instead of reaching into the orchestrator.) +#[cfg(target_os = "windows")] +pub fn console_session_mismatch() -> Option<(u32, u32)> { + use windows::Win32::System::RemoteDesktop::{ + ProcessIdToSessionId, WTSGetActiveConsoleSessionId, + }; + use windows::Win32::System::Threading::GetCurrentProcessId; + let mut own: u32 = 0; + // SAFETY: `own` is a live local out-param for this synchronous call; no pointer escapes it. + if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut own) }.is_err() { + return None; + } + // SAFETY: takes no arguments and returns the console session id by value. + let console = unsafe { WTSGetActiveConsoleSessionId() }; + (console != 0xFFFF_FFFF && own != console).then_some((own, console)) +} diff --git a/crates/pf-win-display/src/win_display.rs b/crates/pf-win-display/src/win_display.rs index c3dac18b..db42019c 100644 --- a/crates/pf-win-display/src/win_display.rs +++ b/crates/pf-win-display/src/win_display.rs @@ -776,6 +776,36 @@ pub unsafe fn source_desktop_rect(target_id: u32) -> Option<(i32, i32, i32, i32) None } +/// The union of every ACTIVE path's source rect — the virtual-desktop bounds `(x, y, w, h)` in +/// desktop coordinates. Read from CCD rather than `GetSystemMetrics(SM_*VIRTUALSCREEN)` so the +/// answer is the CONSOLE's real layout even when the calling process sits in another session (the +/// GDI metrics are a per-session view; the CCD database is global). `None` when nothing is active +/// or the query fails. Used to normalize desktop coordinates into the virtual HID pointer's +/// absolute `0..=32767` axis space (win32k maps the device's logical extents onto the virtual +/// screen). +pub unsafe fn desktop_bounds() -> Option<(i32, i32, i32, i32)> { + let (paths, modes) = query_active_config()?; + let mut acc: Option<(i32, i32, i32, i32)> = None; // (x0, y0, x1, y1) + for p in &paths { + if p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0 { + continue; + } + let idx = p.sourceInfo.Anonymous.modeInfoIdx as usize; + let Some(m) = modes.get(idx) else { continue }; + if m.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE { + continue; + } + let sm = m.Anonymous.sourceMode; + let (x0, y0) = (sm.position.x, sm.position.y); + let (x1, y1) = (x0 + sm.width as i32, y0 + sm.height as i32); + acc = Some(match acc { + None => (x0, y0, x1, y1), + Some((ax0, ay0, ax1, ay1)) => (ax0.min(x0), ay0.min(y0), ax1.max(x1), ay1.max(y1)), + }); + } + acc.map(|(x0, y0, x1, y1)| (x0, y0, x1 - x0, y1 - y0)) +} + /// Place each managed virtual target's SOURCE at the given desktop-space origin, as ONE atomic CCD /// `SetDisplayConfig` (design `display-management.md` §6.2 — the Windows arm of the pure /// `vdisplay/layout.rs` arrangement; positions come from `arrange`, this only commits them). Windows