feat(pf-frame,pf-win-display): leaf primitives for the lid-closed first-frame fix
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 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,21 @@ mod imp {
|
|||||||
fn GetCurrentProcess() -> Handle;
|
fn GetCurrentProcess() -> Handle;
|
||||||
fn SetPriorityClass(hProcess: Handle, dwPriorityClass: u32) -> Bool;
|
fn SetPriorityClass(hProcess: Handle, dwPriorityClass: u32) -> Bool;
|
||||||
fn SetThreadExecutionState(esFlags: u32) -> u32;
|
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")]
|
#[link(name = "dwmapi")]
|
||||||
extern "system" {
|
extern "system" {
|
||||||
@@ -46,6 +61,61 @@ mod imp {
|
|||||||
const ES_CONTINUOUS: u32 = 0x8000_0000;
|
const ES_CONTINUOUS: u32 = 0x8000_0000;
|
||||||
const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001;
|
const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001;
|
||||||
const ES_DISPLAY_REQUIRED: u32 = 0x0000_0002;
|
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<DisplayWakeRequest> {
|
||||||
|
let reason: Vec<u16> = "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();
|
static PROCESS_TUNED: OnceLock<()> = OnceLock::new();
|
||||||
|
|
||||||
@@ -94,7 +164,7 @@ mod imp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[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
|
/// No-op on non-Windows (Linux uses `setpriority` nice + CUDA stream priority instead — see
|
||||||
/// `native::boost_thread_priority` and `zerocopy::cuda`).
|
/// `native::boost_thread_priority` and `zerocopy::cuda`).
|
||||||
|
|||||||
@@ -29,4 +29,7 @@ windows = { version = "0.62", features = [
|
|||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
|
# console_session_mismatch: WTSGetActiveConsoleSessionId + ProcessIdToSessionId + GetCurrentProcessId.
|
||||||
|
"Win32_System_RemoteDesktop",
|
||||||
|
"Win32_System_Threading",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -15,3 +15,25 @@ pub mod display_events;
|
|||||||
pub mod monitor_devnode;
|
pub mod monitor_devnode;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub mod win_display;
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -776,6 +776,36 @@ pub unsafe fn source_desktop_rect(target_id: u32) -> Option<(i32, i32, i32, i32)
|
|||||||
None
|
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
|
/// 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
|
/// `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
|
/// `vdisplay/layout.rs` arrangement; positions come from `arrange`, this only commits them). Windows
|
||||||
|
|||||||
Reference in New Issue
Block a user