diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index 7dd948b..79497d1 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -2,10 +2,9 @@ //! CPU-copy fallback (the portal delivers a CPU buffer; the encoder uploads it to the GPU //! internally). Zero-copy dmabuf→NVENC import is deferred (plan §9 risk). -// This file's own unsafe block carries a `// SAFETY:` proof, but the file-level -// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it -// would propagate the lint to `capture::windows::idd_push` (in-flight parallel work, not yet -// proven). The deny lands here once every child module (incl. idd_push.rs) is documented. +// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof +// program). As a parent module this also covers the child modules (capture::windows/linux::*). +#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::Result; diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs index 6f21adf..e599cf0 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs @@ -10,6 +10,9 @@ //! [`pf_driver_proto::frame`] (which OWNS the contract, with `const` size asserts) — both sides //! `use` it, so drift is a compile error rather than a "must match" comment. +// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). +#![deny(clippy::undocumented_unsafe_blocks)] + use super::dxgi::{make_device, D3d11Frame, HdrP010Converter, VideoConverter, WinCaptureTarget}; use super::{CapturedFrame, Capturer, FramePayload, PixelFormat}; use anyhow::{bail, Context, Result}; @@ -225,7 +228,12 @@ pub struct IddPushCapturer { status_logged: bool, _keepalive: Box, } -// COM objects used only from the owning (encode) thread. +// SAFETY: `IddPushCapturer` is `!Send` only because of its `*mut SharedHeader`/`*mut DebugBlock` raw +// pointers (and the COM interfaces). It is created, used, and dropped by a SINGLE thread — the owning +// capture/encode thread — never shared: the `ID3D11DeviceContext` is the device's IMMEDIATE context +// (single-threaded by D3D11 contract) and is only ever touched from that thread, and the header/ +// dbg_block pointers (into mappings this struct owns) are only dereferenced there. `Send` transfers +// ownership to one thread at a time with NO concurrent access; we do not (and must not) claim `Sync`. unsafe impl Send for IddPushCapturer {} /// Build a permissive (Everyone:GenericAll) `SECURITY_ATTRIBUTES` so the restricted WUDFHost driver @@ -343,6 +351,9 @@ impl IddPushCapturer { // a fullscreen game can hold the virtual display at a different mode (esp. across a reconnect), so // matching the actual mode lets the first frame flow instead of being dropped (game-capture bug // GB1). Falls back to the negotiated mode when the CCD read is unavailable. + // SAFETY: `active_resolution` is an `unsafe fn` (Win32 CCD `QueryDisplayConfig`) that takes only a + // copy of the plain `u32` CCD target id and returns owned `(w, h)` values; it forms no borrows from + // us and validates the id internally, returning `None` on any failure (handled by `unwrap_or`). let (w, h) = unsafe { crate::win_display::active_resolution(target.target_id) }.unwrap_or((pw, ph)); if (w, h) != (pw, ph) { @@ -361,6 +372,27 @@ impl IddPushCapturer { // PROACTIVELY enable advanced color so HDR streams without the user toggling anything; an // SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze, // if the user does enable HDR). + // SAFETY: one block over the whole ring setup; every operation in it is sound: + // - `set_advanced_color`/`advanced_color_enabled` are `unsafe fn`s taking only a copy of the plain + // `u32` target id; they read/flip CCD display config and return owned values, borrowing nothing. + // - `CreateDXGIFactory1`, `EnumAdapterByLuid`, `make_device`, `permissive_sa`, `CreateFileMappingW`, + // `MapViewOfFile`, `CreateEventW`, and `create_ring_slots` are all `?`-checked, so every returned + // interface/handle/view is non-error before use; `&sa`/`&adapter`/`&device`/the `&HSTRING` names + // are live borrows that outlive each synchronous call, and `sa.lpSecurityDescriptor` stays valid + // because its backing `_psd` is held in scope for the whole block. + // - The header mapping is created AND viewed at `bytes == size_of::().max(64)`; the + // view's null is checked (`bail!` on failure, after which the owned `map` closes the mapping). The + // OS view base is page-aligned, so `section.ptr::()` is suitably aligned for a + // `SharedHeader`, and `write_bytes(.., 0, bytes)` plus the `(*header).field = ..` writes all stay + // within those `bytes` and write THROUGH the raw pointer without forming any `&mut`. The debug + // section is the same pattern at `dbg_bytes == size_of::()`, only entered when its + // own view is non-null. + // - The `magic` publish stores through `addr_of!((*header).magic) as *const AtomicU32`: `addr_of!` + // takes the field address without a reference; the field is a 4-aligned `u32` (valid for + // `AtomicU32`), and the `Release` store after the `Release` fence is the cross-process handshake + // that orders all preceding writes before the driver may observe `MAGIC`. + // - `header`/`dbg_block` point into the OS mappings, NOT into the `MappedSection` structs, so moving + // `section`/`dbg_section` into `me` leaves them valid (see the `MappedSection` doc comment). unsafe { // If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and // size the ring FP16 directly — don't race the advanced_color_enabled poll, which may not have @@ -541,10 +573,16 @@ impl IddPushCapturer { 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 + // 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 + // no reference into the shared region is formed. Plain read: the driver writes this `u32` + // cross-process, but an aligned `u32` read can't tear and `driver_status` is best-effort + // diagnostics — the real handshake is the atomic `magic`/`latest` (same access as // log_driver_status_once). let st = unsafe { (*self.header).driver_status }; if matches!(st, DRV_STATUS_TEX_FAIL | DRV_STATUS_NO_DEVICE1) { + // SAFETY: as above — an in-bounds, aligned `u32` read of a best-effort diagnostic field + // through the owned, live header mapping; no reference into the shared region is formed. let detail = unsafe { (*self.header).driver_status_detail }; bail!( "IDD-push driver failed to attach (driver_status={st} detail=0x{detail:08x} — \ @@ -567,6 +605,10 @@ impl IddPushCapturer { #[inline] fn latest(&self) -> u64 { + // SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a + // `SharedHeader`). `addr_of!((*self.header).latest)` forms the address of the `latest` field + // WITHOUT a reference; it is an 8-aligned `u64` (so valid for `AtomicU64`), and the `Acquire` load + // is the consumer half of the cross-process publish handshake (pairs with the driver's `Release`). unsafe { (*(std::ptr::addr_of!((*self.header).latest) as *const AtomicU64)) .load(Ordering::Acquire) @@ -578,6 +620,10 @@ impl IddPushCapturer { if self.status_logged { return; } + // SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping. The driver writes + // these `u32`/`i32` diagnostic fields cross-process, but aligned word reads can't tear and these are + // best-effort status (the real handshake is the atomic `magic`/`latest`); no `&`/`&mut` reference + // into the shared region is formed. let (status, detail, lo, hi) = unsafe { ( (*self.header).driver_status, @@ -617,6 +663,11 @@ impl IddPushCapturer { tracing::warn!("IDD push DEBUG: no debug block"); return; } + // SAFETY: `self.dbg_block` was just checked non-null (the early return above); it points into the + // owned `dbg_section` mapping sized exactly `size_of::()` and page-aligned, so it is + // valid + aligned for `DebugBlock`. `d` is a short-lived SHARED reference used only to read the + // fields below; we never form `&mut` into this region, and the driver's cross-process writes are + // aligned `u32`s that don't tear (best-effort bring-up diagnostics). let d = unsafe { &*self.dbg_block }; tracing::error!( run_core_entries = d.run_core_entries, @@ -666,6 +717,10 @@ impl IddPushCapturer { self.height = new_h; let fmt = self.ring_format(); let new_gen = IDD_GENERATION.fetch_add(1, Ordering::Relaxed); + // SAFETY: `create_ring_slots` is an `unsafe fn` (it makes D3D11/DXGI COM calls); we pass a live + // borrow of `self.device` (the capturer's own device, on which the slots are created) plus plain + // `u32`/`DXGI_FORMAT` values, and `?` propagates any failure before the slots are used. Every + // returned slot's texture + keyed mutex belongs to that same `self.device`. let new_slots = unsafe { Self::create_ring_slots( &self.device, @@ -676,6 +731,12 @@ impl IddPushCapturer { fmt, )? }; + // SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a + // `SharedHeader`). The `latest`/`generation` stores go through `addr_of!`-formed field pointers (no + // references) of correctly-aligned `u64`/`u32` fields, valid for `AtomicU64`/`AtomicU32`; the + // `dxgi_format`/`width`/`height` writes are in-bounds raw writes through the pointer (no `&mut`). + // The `Release` fence + the `Release` `generation` store publish all preceding writes so the driver + // only re-attaches (`Acquire`) once the new textures + format are in place. unsafe { // Clear `latest` to the 0 sentinel (generation 0, which try_consume rejects). The real guard // against consuming an unwritten new-ring slot is the generation tag in `latest`: a stale @@ -711,9 +772,13 @@ impl IddPushCapturer { return; } self.last_acm_poll = Instant::now(); + // SAFETY: `advanced_color_enabled` is an `unsafe fn` taking only a copy of the plain `u32` target + // id; it performs a read-only CCD query and returns an owned `bool`, borrowing nothing from us. let now_hdr = unsafe { crate::win_display::advanced_color_enabled(self.target_id) }; // Follow the display's ACTUAL resolution too — a fullscreen game can mode-set the virtual display // out from under the negotiated size (game-capture bug GB1). Unknown read → keep our current size. + // SAFETY: `active_resolution` is an `unsafe fn` taking only a copy of the plain `u32` target id; it + // performs a read-only CCD query and returns owned `(w, h)` values, borrowing nothing from us. let (now_w, now_h) = unsafe { crate::win_display::active_resolution(self.target_id) } .unwrap_or((self.width, self.height)); if now_hdr == self.display_hdr && now_w == self.width && now_h == self.height { @@ -760,6 +825,10 @@ impl IddPushCapturer { }; for _ in 0..OUT_RING { let mut t: Option = None; + // SAFETY: `CreateTexture2D` is called on `self.device` (the capturer's live D3D11 device); + // `&desc` is a fully-initialized stack `D3D11_TEXTURE2D_DESC`, the data arg is `None` (no + // initial data), and `Some(&mut t)` is a live out-parameter the call fills. `?` rejects a failed + // HRESULT before `t` is unwrapped, and the created texture belongs to `self.device`. unsafe { self.device .CreateTexture2D(&desc, None, Some(&mut t)) @@ -775,9 +844,16 @@ impl IddPushCapturer { fn ensure_converter(&mut self) -> Result<()> { if self.display_hdr { if self.hdr_p010_conv.is_none() { + // SAFETY: `HdrP010Converter::new` is `unsafe` (it compiles D3D11 shaders + creates + // resources); we pass a live borrow of `self.device`, the device the converter's resources + // belong to, and `?` propagates any failure before the converter is stored. self.hdr_p010_conv = Some(unsafe { HdrP010Converter::new(&self.device)? }); } } else if self.video_conv.is_none() { + // SAFETY: `VideoConverter::new` is `unsafe` (it sets up the D3D11 VIDEO processor); we pass live + // borrows of `self.device` + its immediate `self.context` (single-threaded, this thread) plus + // plain `u32` dimensions, and `?` propagates any failure before it is stored. The converter's + // resources belong to that same device/context. self.video_conv = Some(unsafe { VideoConverter::new(&self.device, &self.context, self.width, self.height, false)? }); @@ -942,6 +1018,8 @@ pub fn spawn_observer(target: WinCaptureTarget, preferred: Option<(u32, u32, u32 /// The discrete render GPU LUID (where NVENC runs), falling back to the monitor's `OsAdapterLuid`. fn resolve_render_adapter_luid_or(fallback_packed: i64) -> LUID { + // SAFETY: `resolve_render_adapter_luid` is an `unsafe fn` (it enumerates DXGI adapters) that takes no + // arguments and returns an owned `Option`, borrowing nothing. if let Some(l) = unsafe { crate::win_adapter::resolve_render_adapter_luid() } { return l; } @@ -955,6 +1033,9 @@ impl Capturer for IddPushCapturer { fn next_frame(&mut self) -> Result { let deadline = Instant::now() + Duration::from_secs(20); loop { + // SAFETY: `self.event` is the live frame-ready `OwnedHandle` this capturer owns; its raw value + // (borrowed for the call, so it outlives this synchronous wait) is a valid auto-reset event + // handle. `WaitForSingleObject` only reads the handle; the 16 ms timeout bounds the wait. let _ = unsafe { WaitForSingleObject(HANDLE(self.event.as_raw_handle()), 16) }; if let Some(f) = self.try_consume()? { return Ok(f); @@ -964,6 +1045,9 @@ impl Capturer for IddPushCapturer { } if Instant::now() > deadline { self.log_debug_block(); + // SAFETY: four in-bounds, aligned reads of the live, owned shared-header mapping — the same + // best-effort diagnostic fields as `log_driver_status_once` (aligned word reads can't tear; + // no reference into the shared region is formed). let (st, detail, lo, hi) = unsafe { ( (*self.header).driver_status, diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index e48a740..a463f6c 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -3,10 +3,9 @@ //! RGB→YUV on the GPU, so no host-side CSC) and VAAPI on AMD/Intel (`*_vaapi`; the CPU-input //! fallback swscales RGB→NV12, the zero-copy path imports the capture dmabuf straight into a //! VA surface). One [`Encoder`] trait, selected in [`open_video`]. -// This file's own unsafe block carries a `// SAFETY:` proof, but the file-level -// `#![deny(clippy::undocumented_unsafe_blocks)]` is deliberately NOT set yet: as a parent module it -// would propagate the lint to `encode::windows::nvenc` (in-flight parallel work, not yet proven). -// The deny lands here once every child module (incl. nvenc.rs) is documented. +// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof +// program). As a parent module this also covers the child modules (encode::windows/linux::*). +#![deny(clippy::undocumented_unsafe_blocks)] use crate::capture::{CapturedFrame, PixelFormat}; use anyhow::Result; diff --git a/crates/punktfunk-host/src/encode/windows/nvenc.rs b/crates/punktfunk-host/src/encode/windows/nvenc.rs index 076d5d8..d287118 100644 --- a/crates/punktfunk-host/src/encode/windows/nvenc.rs +++ b/crates/punktfunk-host/src/encode/windows/nvenc.rs @@ -13,6 +13,9 @@ //! Needs a real NVIDIA GPU at runtime (session creation fails otherwise) — compiles GPU-less, but //! `open`/`submit` only succeed on a GPU box. The software encoder (`super::sw`) is the fallback. +// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. +#![deny(clippy::undocumented_unsafe_blocks)] + use super::{Codec, EncodedFrame, Encoder, EncoderCaps}; use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use anyhow::{anyhow, bail, Context, Result}; @@ -88,7 +91,15 @@ pub struct NvencD3d11Encoder { init_device: *mut c_void, } -// Raw NVENC handle + COM ptrs; confined to the single encode thread (like the Linux encoder). +// SAFETY: the `!Send` fields are the raw NVENC session/device handles (`encoder`, `init_device`), +// the raw NVENC bitstream/registered/mapped pointers carried in `bitstreams`/`regs`/`pending`, and +// the `ID3D11Texture2D` COM refs — none of which may be touched concurrently from two threads. This +// encoder is owned by exactly one thread: it is moved onto the host encode thread once at +// construction, and every NVENC call and D3D11 access happens only from that thread thereafter +// (`submit`/`poll`/`invalidate_ref_frames`/`Drop` all run there, like the Linux encoder). Moving the +// handles across that single ownership-transfer boundary is sound because no NVENC/D3D11 call is in +// flight during the move and the session and its D3D11 immediate context are never shared (`&`) or +// used concurrently — so `Send` introduces no data race on the non-`Send` fields. unsafe impl Send for NvencD3d11Encoder {} impl NvencD3d11Encoder { @@ -403,6 +414,17 @@ impl NvencD3d11Encoder { /// Lazily create the session on the first frame's D3D11 device (so capture + encode share it). fn init_session(&mut self, device: &ID3D11Device) -> Result<()> { + // SAFETY: every call below goes through a function pointer resolved once from the loaded + // `nvidia_video_codec_sdk::ENCODE_API` (`nvEncodeAPI`) table, or through this type's own + // `unsafe fn`s whose contract is met here. `query_caps`/`try_open_session` receive `device`, + // the live `ID3D11Device` the caller pulled off the first frame; each returns either a valid + // open NVENC session handle or an `Err`. `destroy_encoder` is only ever called on a handle a + // `try_open_session` just returned (and `best` only when `!best.is_null()`), so it never frees + // a dangling or null session. `create_bitstream_buffer` is passed `enc` — the one chosen live + // session — and `&mut cb`, a `#[repr(C)] NV_ENC_CREATE_BITSTREAM_BUFFER` whose `version` is set + // to `NV_ENC_CREATE_BITSTREAM_BUFFER_VER`; `cb` lives across the synchronous call and its + // returned `bitstreamBuffer` is copied into `self.bitstreams` before `cb` drops. No handle + // escapes the encode thread. unsafe { // Probe real GPU caps first (max dims / 10-bit / custom-VBV / RFI) so the config below is // gated on what this card supports and an out-of-range mode fails with a clear error @@ -589,6 +611,11 @@ impl Encoder for NvencD3d11Encoder { new = format!("{}x{}", captured.width, captured.height), "NVENC: capture device/size/HDR changed — re-initializing session" ); + // SAFETY: `teardown` (an `unsafe fn`) requires the encode thread with no NVENC call in + // flight and a session whose cached regs/bitstreams/pending all belong to `self.encoder`. + // All hold: this is the synchronous encode thread, `self.inited` so `self.encoder` is the + // live session every cached resource was created against, and the previous frame's encode + // has already been polled (synchronous submit→poll), so nothing is mid-encode. unsafe { self.teardown() }; } if !self.inited { @@ -625,6 +652,21 @@ impl Encoder for NvencD3d11Encoder { } let slot = self.next % POOL; self.next += 1; + // SAFETY: every NVENC call goes through a function pointer from the loaded `ENCODE_API` table + // and takes `self.encoder`, the live session `init_session` just established (non-null on the + // path that reaches here). `NV_ENC_REGISTER_RESOURCE rr` has `version = + // NV_ENC_REGISTER_RESOURCE_VER` and registers `frame.texture` — a D3D11 texture from + // `frame.device`, which is the SAME device the session was opened against (any device change + // tears down and re-inits above, so `init_device == frame.device.as_raw()` here); the cloned + // `ID3D11Texture2D` is kept alive in `regs` so NVENC's registration never outlives the texture. + // `mp` (`NV_ENC_MAP_INPUT_RESOURCE`, version set) maps that registration and the map is recorded + // in `pending` to be unmapped exactly once in `poll`/`teardown`. `pic` (`NV_ENC_PIC_PARAMS`, + // version set) points `inputBuffer` at `mp.mappedResource` and `outputBitstream` at the live + // pool bitstream `bitstreams[slot]`; the optional SEI scratch (`mastering_sei`/`cll_sei` and the + // `sei` Vec whose `as_mut_ptr()` is written into the codec union) are stack locals that outlive + // the synchronous `encode_picture`. Every `#[repr(C)]` param is a live local borrowed `&mut` + // for the duration of its one synchronous call. (In-place encode without `CopyResource` is + // sound because the encode loop is synchronous, as the module docs state.) unsafe { // Register the capturer's texture with NVENC once (cached by raw pointer), then encode it // IN PLACE — no `CopyResource` into an encoder-owned pool. This is the zero-copy win: the @@ -781,6 +823,12 @@ impl Encoder for NvencD3d11Encoder { // We tag each input with `inputTimeStamp = frame_idx` (0,1,2,…), which is also the client's // frame number (the packetizer numbers frames in submit order), so the client's lost-frame // range maps 1:1 onto the timestamps NVENC invalidates here. + // SAFETY: `invalidate_ref_frames` is a function pointer from the loaded `ENCODE_API` table. + // `self.encoder` was checked non-null at the top of this fn and is the live session; this runs + // on the encode thread (like submit/poll), so there is no concurrent NVENC use. Each `ts` was + // clamped to `[oldest_in_dpb, frame_idx - 1]` above, so it names a frame still in the session's + // DPB; the call passes only that `u64` timestamp (no struct), so there is no struct-size or + // lifetime concern. unsafe { for ts in first..=last { if (API.invalidate_ref_frames)(self.encoder, ts as u64) @@ -799,6 +847,16 @@ impl Encoder for NvencD3d11Encoder { let Some((bs, map, pts_ns)) = self.pending.pop_front() else { return Ok(None); }; + // SAFETY: a non-empty `pending` implies `submit` ran, so `self.encoder` is the live session + // (`teardown` clears `pending` whenever it nulls the handle); all calls below use function + // pointers from the loaded `ENCODE_API` table on the encode thread. `NV_ENC_LOCK_BITSTREAM lock` + // (version = `NV_ENC_LOCK_BITSTREAM_VER`) locks `bs`, a pool bitstream a prior `encode_picture` + // targeted; `lock_bitstream` blocks until that encode finishes, so on success + // `lock.bitstreamBufferPtr` is non-null and points at `lock.bitstreamSizeInBytes` bytes of + // NVENC-owned, CPU-readable output valid until `unlock_bitstream`. The `from_raw_parts` slice is + // only read (copied via `to_vec()`) BEFORE `unlock_bitstream(bs)` — lock and unlock pair on the + // same buffer — so it never outlives the lock. `map` (the input resource paired with `bs` in + // `pending`) is unmapped here, after the encode completed, exactly once. unsafe { let mut lock = nv::NV_ENC_LOCK_BITSTREAM { version: nv::NV_ENC_LOCK_BITSTREAM_VER, @@ -838,6 +896,11 @@ impl Encoder for NvencD3d11Encoder { impl Drop for NvencD3d11Encoder { fn drop(&mut self) { + // SAFETY: `teardown` (an `unsafe fn`) needs the owning thread with no NVENC call in flight and + // a session whose cached resources all belong to `self.encoder`. At Drop this encoder is owned + // exclusively (no other reference can exist), runs on the encode thread it was confined to, and + // `teardown` early-returns when `self.encoder` is null; otherwise every cached reg/bitstream/ + // pending was created against that live session. It runs exactly once (here). unsafe { self.teardown() }; } } diff --git a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs b/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs index 748b32c..67c6645 100644 --- a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs @@ -41,7 +41,8 @@ pub(super) const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS" pub(super) const OFF_INPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, input); pub(super) const OFF_OUT_SEQ: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, out_seq); -pub(super) const OFF_OUTPUT: usize = core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, output); +pub(super) const OFF_OUTPUT: usize = + core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, output); /// Device-type selector the driver reads to choose which HID identity/descriptor it serves: 0 = /// DualSense (the default — the section is zeroed), 1 = DualShock 4. pub(super) const OFF_DEVTYPE: usize = diff --git a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs b/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs index dd75f6c..bd2f3f3 100644 --- a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs @@ -187,8 +187,10 @@ impl XusbWinPad { #[allow(clippy::too_many_arguments)] fn write_state(&mut self, buttons: u16, lt: u8, rt: u8, lx: i16, ly: i16, rx: i16, ry: i16) { self.packet = self.packet.wrapping_add(1); - // SAFETY: base points at SHM_SIZE bytes; all offsets are in range. let base = self.shm.base(); + // SAFETY: `base` is the start of the mapped section (`SHM_SIZE` bytes, owned by `Shm`); every + // `OFF_*` is a fixed in-range offset into it and `write_unaligned` handles the unaligned field + // writes. Single owner (`&mut self`), so no concurrent writer races these stores. unsafe { std::ptr::write_unaligned(base.add(OFF_BUTTONS) as *mut u16, buttons); *base.add(OFF_LT) = lt; diff --git a/crates/punktfunk-host/src/inject/windows/sendinput.rs b/crates/punktfunk-host/src/inject/windows/sendinput.rs index fe0d2c6..b09d54f 100644 --- a/crates/punktfunk-host/src/inject/windows/sendinput.rs +++ b/crates/punktfunk-host/src/inject/windows/sendinput.rs @@ -238,7 +238,8 @@ impl InputInjector for SendInputInjector { } InputKind::KeyDown | InputKind::KeyUp => { let down = event.kind == InputKind::KeyDown; - let vk = (event.code & 0xff) as u16; // client sends Windows VK + // client sends Windows VK + let vk = (event.code & 0xff) as u16; // SAFETY: `MapVirtualKeyExW` is a pure value translation (VK → scancode); all three // args are by-value (`u32`, the `MAPVK_VK_TO_VSC_EX` map-type constant, a `None` // HKL). It dereferences no pointer and returns a `u32` — FFI-`unsafe` only. diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 007a13f..026418a 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -13,6 +13,10 @@ // Scaffold: trait methods and config paths are defined ahead of their backends. #![allow(dead_code)] +// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in the crate must carry a `// SAFETY:` +// proof of why it is sound. This crate-root deny is the permanent, catch-all gate (it also covers +// any future module); individual files keep their own `#![deny(...)]` as belt-and-suspenders. +#![deny(clippy::undocumented_unsafe_blocks)] mod audio; mod capture; diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 9bcb47b..0890bb3 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -22,6 +22,9 @@ //! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared //! with GameStream pairing) and logs the SHA-256 fingerprint clients pin. +// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). +#![deny(clippy::undocumented_unsafe_blocks)] + use anyhow::{anyhow, Context, Result}; use punktfunk_core::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Role}; use punktfunk_core::input::{InputEvent, InputKind}; @@ -1965,6 +1968,11 @@ pub(crate) fn boost_thread_priority(critical: bool) { // capture/encode (critical) and send (non-critical). crate::session_tuning::on_hot_thread(); #[cfg(target_os = "windows")] + // SAFETY: `GetCurrentThread()` returns the constant pseudo-handle for the calling thread — always + // valid, thread-local in meaning, and never closed (no leak/double-close). `SetThreadPriority` + // takes that handle plus a `THREAD_PRIORITY_*` value the windows crate defines (HIGHEST or + // ABOVE_NORMAL here); it only reprioritizes this OS thread, borrows no Rust memory, and its + // `Result` is matched (a failure is logged, never UB). No pointers, lifetimes, or aliasing. unsafe { use windows::Win32::System::Threading::{ GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL, @@ -1992,6 +2000,10 @@ pub(crate) fn boost_thread_priority(critical: bool) { // realtime CPU class can preempt the compositor AND the game's own render thread, adding the // very frame-time we refuse to add (opt-in only — see PUNKTFUNK_SCHED_RR). let nice = if critical { -10 } else { -5 }; + // SAFETY: `setpriority` takes three by-value integers and no pointers, so there is nothing to + // alias or outlive. `PRIO_PROCESS` with `who == 0` targets the calling task on Linux and + // `nice` is in range; the call only adjusts this thread's scheduling nice value and returns an + // `int` we inspect. No memory is touched. let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, nice) }; if rc == 0 { tracing::debug!(critical, nice, "thread nice raised"); @@ -2707,6 +2719,11 @@ fn virtual_stream_relay(ctx: SessionContext) -> Result<()> { // The secure-desktop HDR drop (for the DDA leg) keys off the monitor's real state in the mux loop. #[cfg(target_os = "windows")] if bit_depth >= 10 { + // SAFETY: `set_advanced_color` is marked `unsafe` only because it drives the Win32 CCD API + // internally; it takes `target_id` by value (Copy `u32` — this session's live SudoVDA + // monitor's CCD target id) and sizes + owns every buffer it hands the OS on its own stack. + // We pass no pointers, so nothing must outlive the call and there is no aliasing; an + // unknown/absent target id simply returns false. unsafe { if crate::win_display::set_advanced_color(target.target_id, true) { // Let the colorspace change settle before WGC creates its capture item / detects HDR. @@ -2942,8 +2959,12 @@ fn virtual_stream_relay(ctx: SessionContext) -> Result<()> { // desktop (the drop just churned + still went black). Instead, if the monitor is in HDR, // open DDA in HDR (FP16 DuplicateOutput1 → BT.2020 PQ Main10); the normal-desktop DDA // overlay/flip issues that drove us to WGC don't apply to the composed Winlogon UI. - let hdr = - unsafe { crate::win_display::advanced_color_enabled(target.target_id) }; + // SAFETY: `advanced_color_enabled` is `unsafe` only because it queries the Win32 CCD + // API; it takes `target_id` by value (the live SudoVDA monitor's CCD target id) and + // allocates + owns every buffer it passes the OS internally. No caller pointer is + // involved, so nothing must outlive the call and there is no aliasing; a missing + // target id just yields false. + let hdr = unsafe { crate::win_display::advanced_color_enabled(target.target_id) }; dda = None; // reopen to capture the secure desktop match open_dda(&target, cur_mode.width, cur_mode.height, effective_hz, hdr) { Ok(mut p) => { @@ -3368,12 +3389,27 @@ mod tests { unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) { use punktfunk_core::error::PunktfunkStatus; let mut got = 0u32; + // SAFETY: the inferred type is the `#[repr(C)]` POD `PunktfunkFrame` (a raw `*const u8`, a + // `usize`, and integer fields); all-zero is a valid bit pattern for every field (a null + // `data`, `len == 0`). It is only ever read after `next_au` below fully overwrites it on `Ok`, + // so the zeroed value is never observed. let mut frame = unsafe { std::mem::zeroed() }; while got < count { + // SAFETY: `conn` is the live, non-null `*mut PunktfunkConnection` from `punktfunk_connect` + // (the caller asserts non-null and does not close it until after this returns), meeting the + // ABI's "valid handle". `&mut frame` is an exclusive, writable borrow of the local + // `PunktfunkFrame` that outlives this synchronous call. This single test thread is the only + // video puller, satisfying the one-video-thread rule. match unsafe { punktfunk_core::abi::punktfunk_connection_next_au(conn, &mut frame, 2000) } { PunktfunkStatus::Ok => { + // SAFETY: on `Ok`, `next_au` set `frame.data`/`frame.len` to the reassembled AU + // buffer the connection owns; per the ABI contract that borrow stays valid until + // the NEXT `next_au` call on this handle. We read the whole slice here (the assert + // + length-checked indexing) before the loop's next `next_au`, and `conn` outlives + // it — so the pointer is live, exactly `len` bytes, read-only, single-threaded (no + // aliasing/use-after-free). let data = unsafe { std::slice::from_raw_parts(frame.data, frame.len) }; let idx = u32::from_le_bytes(data[0..4].try_into().unwrap()); assert_eq!( @@ -3421,6 +3457,11 @@ mod tests { // Session 1: TOFU (no pin) — observe the host fingerprint. let addr = std::ffi::CString::new("127.0.0.1").unwrap(); let mut observed = [0u8; 32]; + // SAFETY: `addr` is a live `CString` ("127.0.0.1") whose `as_ptr()` is the NUL-terminated + // UTF-8 host string the contract requires; `pin_sha256`/cert/key are NULL (all permitted), and + // `observed.as_mut_ptr()` is the local `[u8; 32]` — exactly the 32 writable bytes the contract + // demands, not aliased during the call. Every pointer references a live local that outlives the + // blocking connect. let conn = unsafe { punktfunk_connect( addr.as_ptr(), @@ -3439,26 +3480,28 @@ mod tests { assert_ne!(observed, [0u8; 32], "fingerprint not reported"); let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32); - assert_eq!( - unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) }, - PunktfunkStatus::Ok - ); + // SAFETY: `conn` is the live, non-null connection handle just asserted above; `&mut w/h/hz` are + // exclusive, writable borrows of local `u32`s that outlive this synchronous call — the three + // writable out-params the contract names. + let st = unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) }; + assert_eq!(st, PunktfunkStatus::Ok); assert_eq!((w, h, hz), (1280, 720, 60)); // Mid-stream renegotiation: request a new mode, the host acks on the control // stream, and punktfunk_connection_mode reflects the switch. - assert_eq!( - unsafe { - punktfunk_core::abi::punktfunk_connection_request_mode(conn, 1920, 1080, 144) - }, - PunktfunkStatus::Ok - ); + // SAFETY: `conn` is the live, non-null connection handle (the only pointer arg); the remaining + // arguments are by-value integers. The handle outlives this non-blocking enqueue. + let st = unsafe { + punktfunk_core::abi::punktfunk_connection_request_mode(conn, 1920, 1080, 144) + }; + assert_eq!(st, PunktfunkStatus::Ok); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); loop { - assert_eq!( - unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) }, - PunktfunkStatus::Ok - ); + // SAFETY: same as the earlier `punktfunk_connection_mode` call — `conn` is the live handle + // and `&mut w/h/hz` are exclusive writable borrows of locals that outlive this synchronous + // call. + let st = unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) }; + assert_eq!(st, PunktfunkStatus::Ok); if (w, h, hz) == (1920, 1080, 144) { break; } @@ -3469,6 +3512,8 @@ mod tests { std::thread::sleep(std::time::Duration::from_millis(20)); } + // SAFETY: `pull_verified` requires a live connection handle it alone pulls video from; `conn` is + // the open, non-null handle from `punktfunk_connect` and this is the only thread touching it. unsafe { pull_verified(conn, 25) }; let ev = punktfunk_core::input::InputEvent { @@ -3479,13 +3524,19 @@ mod tests { y: 2, flags: 0, }; - assert_eq!( - unsafe { punktfunk_connection_send_input(conn, &ev) }, - PunktfunkStatus::Ok - ); + // SAFETY: `conn` is the live handle; `&ev` borrows the local `InputEvent`, valid and immutable + // for this synchronous enqueue — the contract's "valid InputEvent" pointer. + let st = unsafe { punktfunk_connection_send_input(conn, &ev) }; + assert_eq!(st, PunktfunkStatus::Ok); + // SAFETY: `conn` was returned by `punktfunk_connect` and is never used after this call (session + // 2 below uses a fresh `conn2`); `close` takes ownership and frees the handle exactly once. unsafe { punktfunk_connection_close(conn) }; // Session 2 (same host process — the listener survived): pin the fingerprint. + // SAFETY: as for session 1 — `addr` is the live NUL-terminated host string; here + // `observed.as_ptr()` is the 32-byte pin (the fingerprint captured above, a valid `[u8; 32]`), + // `observed_sha256_out` is NULL and cert/key are NULL. All pointers reference live locals for + // the duration of the blocking connect. let conn2 = unsafe { punktfunk_connect( addr.as_ptr(), @@ -3501,11 +3552,17 @@ mod tests { ) }; assert!(!conn2.is_null(), "pinned reconnect failed"); + // SAFETY: `conn2` is the live, non-null pinned handle, pulled only from this thread — + // `pull_verified`'s requirement. unsafe { pull_verified(conn2, 25) }; + // SAFETY: `conn2` came from `punktfunk_connect` and is not used after this; `close` frees it once. unsafe { punktfunk_connection_close(conn2) }; // Session 3: a wrong pin must be rejected by the handshake. let bad = [0xAAu8; 32]; + // SAFETY: same shape as the prior connects — `addr` is the live host string, `bad.as_ptr()` is + // the 32-byte `[0xAA; 32]` pin, and out/cert/key are NULL; all reference live locals across the + // blocking call. (The handshake is expected to fail and return NULL here, which is sound.) let conn3 = unsafe { punktfunk_connect( addr.as_ptr(), @@ -3525,6 +3582,8 @@ mod tests { // The host saw the rejected handshake attempt as session 3? No — a TLS-failed // handshake never yields a connection, so accept() is still waiting. Connect once // more (TOFU) to complete the host's third session and let it exit. + // SAFETY: same as session 1's connect — `addr` is the live host string, pin/out/cert/key all + // NULL; the pointers reference live locals for the duration of the blocking connect. let conn4 = unsafe { punktfunk_connect( addr.as_ptr(), @@ -3540,7 +3599,9 @@ mod tests { ) }; assert!(!conn4.is_null()); + // SAFETY: `conn4` is the live, non-null handle, pulled only from this thread. unsafe { pull_verified(conn4, 25) }; + // SAFETY: `conn4` came from `punktfunk_connect` and is unused after this; `close` frees it once. unsafe { punktfunk_connection_close(conn4) }; host.join().unwrap().unwrap(); diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs index fa6c05e..ef7c754 100644 --- a/crates/punktfunk-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -622,12 +622,12 @@ mod gamescope; #[cfg(target_os = "linux")] #[path = "vdisplay/linux/kwin.rs"] mod kwin; -#[cfg(target_os = "linux")] -#[path = "vdisplay/linux/mutter.rs"] -mod mutter; #[cfg(target_os = "windows")] #[path = "vdisplay/windows/manager.rs"] pub(crate) mod manager; +#[cfg(target_os = "linux")] +#[path = "vdisplay/linux/mutter.rs"] +mod mutter; #[cfg(target_os = "windows")] #[path = "vdisplay/windows/pf_vdisplay.rs"] pub(crate) mod pf_vdisplay; diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 85c87fc..0d579f0 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -63,8 +63,12 @@ pub(crate) trait VdisplayDriver: Send + Sync { /// /// # Safety /// `dev` must be the live control handle from [`open`](Self::open). - unsafe fn add_monitor(&self, dev: HANDLE, mode: Mode, render_luid: Option) - -> Result; + unsafe fn add_monitor( + &self, + dev: HANDLE, + mode: Mode, + render_luid: Option, + ) -> Result; /// REMOVE the monitor identified by `key`. /// /// # Safety @@ -150,7 +154,8 @@ pub(crate) fn init(driver: Box) -> &'static VirtualDisplayMa /// The process-wide manager. Panics if reached before a backend called [`init`] — by construction a /// session is only ever created after `vdisplay::open` constructed the backend (which calls `init`). pub(crate) fn vdm() -> &'static VirtualDisplayManager { - VDM.get().expect("VirtualDisplayManager used before a backend initialised it") + VDM.get() + .expect("VirtualDisplayManager used before a backend initialised it") } impl VirtualDisplayManager { @@ -178,9 +183,7 @@ impl VirtualDisplayManager { /// The live control handle for the pinger/linger threads (lock-free: the device never changes once /// opened). `None` only before the first acquire opened it. fn device_handle(&self) -> Option { - self.device - .get() - .map(|d| HANDLE(d.as_raw_handle())) + self.device.get().map(|d| HANDLE(d.as_raw_handle())) } /// Open + initialise the backend (validates the driver is present). Mirrors the old @@ -203,8 +206,7 @@ impl VirtualDisplayManager { // client is gone). A REUSED IddCx swap-chain is DEAD, so joining it hands a black screen — // PREEMPT: tear the old monitor down (its key/topology are restored) and create a fresh one. The // old session's lease is gen-stamped, so its later drop is a no-op and can't tear down the new one. - if idd_push_mode() - && matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. }) + if idd_push_mode() && matches!(*state, MgrState::Active { .. } | MgrState::Lingering { .. }) { if let MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } = std::mem::replace(&mut *state, MgrState::Idle) @@ -235,14 +237,21 @@ impl VirtualDisplayManager { // `Active` state, held under the `state` lock, so nothing else reconfigures it concurrently. unsafe { self.reconfigure(mon, mode) }; } - tracing::info!(refs = *refs, backend = self.driver.name(), "virtual monitor reused (concurrent / reconfigure session)"); + tracing::info!( + refs = *refs, + backend = self.driver.name(), + "virtual monitor reused (concurrent / reconfigure session)" + ); return Ok(self.output_for(mon)); } // Idle or Lingering: repurpose a lingering monitor / create a fresh one → Active{refs:1}. let mon = match std::mem::replace(&mut *state, MgrState::Idle) { MgrState::Lingering { mut mon, .. } => { - tracing::info!(backend = self.driver.name(), "virtual monitor reused (reconnect within the linger window)"); + tracing::info!( + backend = self.driver.name(), + "virtual monitor reused (reconnect within the linger window)" + ); if mon.mode != mode { // SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live // display topology. `mon` is the local monitor just moved out of the `Lingering` @@ -291,7 +300,8 @@ impl VirtualDisplayManager { // Mandatory keepalive: ping inside the watchdog window or the driver tears all displays down. // The pinger reaches the singleton for both the device + the driver — no raw-handle smuggle. let stop = Arc::new(AtomicBool::new(false)); - let interval = Duration::from_millis(self.watchdog_s.load(Ordering::Relaxed) as u64 * 1000 / 3); + let interval = + Duration::from_millis(self.watchdog_s.load(Ordering::Relaxed) as u64 * 1000 / 3); let stop_t = stop.clone(); let pinger = thread::spawn(move || { let mut warned = false; @@ -374,7 +384,10 @@ impl VirtualDisplayManager { /// Touches the live display topology via the CCD/GDI helpers. unsafe fn reconfigure(&self, mon: &mut Monitor, mode: Mode) { tracing::info!( - old = format!("{}x{}@{}", mon.mode.width, mon.mode.height, mon.mode.refresh_hz), + old = format!( + "{}x{}@{}", + mon.mode.width, mon.mode.height, mon.mode.refresh_hz + ), new = format!("{}x{}@{}", mode.width, mode.height, mode.refresh_hz), "virtual-display: reconfiguring reused monitor to the new client mode" ); @@ -408,7 +421,10 @@ impl VirtualDisplayManager { if let Err(e) = unsafe { self.driver.remove_monitor(dev, &mon.key) } { tracing::warn!("virtual-display REMOVE failed: {e:#}"); } else { - tracing::info!(backend = self.driver.name(), "virtual-display monitor removed"); + tracing::info!( + backend = self.driver.name(), + "virtual-display monitor removed" + ); } } @@ -425,10 +441,16 @@ impl VirtualDisplayManager { return; } *state = match std::mem::replace(&mut *state, MgrState::Idle) { - MgrState::Active { mon, refs } if refs > 1 => MgrState::Active { mon, refs: refs - 1 }, + MgrState::Active { mon, refs } if refs > 1 => MgrState::Active { + mon, + refs: refs - 1, + }, MgrState::Active { mon, .. } => { let ms = linger_ms(); - tracing::info!(linger_ms = ms, "virtual-display: last session left — lingering before teardown"); + tracing::info!( + linger_ms = ms, + "virtual-display: last session left — lingering before teardown" + ); MgrState::Lingering { mon, until: Instant::now() + Duration::from_millis(ms), diff --git a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs index 8811097..87d93aa 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs @@ -238,14 +238,13 @@ impl VdisplayDriver for PfVdisplayDriver { // borrows the local `AddRequest` (alive across this synchronous call) as the input bytes, and // `out` is a stack `[u8; size_of::()]` whose length bounds the kernel's write — both // buffers outlive the call. - unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) }.with_context( - || { + unsafe { ioctl(dev, control::IOCTL_ADD, bytemuck::bytes_of(&add), &mut out) } + .with_context(|| { format!( "pf-vdisplay ADD {}x{}@{}", mode.width, mode.height, mode.refresh_hz ) - }, - )?; + })?; // `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte // alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`. let reply: control::AddReply = @@ -291,7 +290,15 @@ impl VdisplayDriver for PfVdisplayDriver { // SAFETY: per `remove_monitor`'s contract `dev` is the live control handle. `bytes_of(&req)` // borrows the local `RemoveRequest` for the duration of this synchronous call as the input // bytes; `none` is empty, so there is no output buffer. - unsafe { ioctl(dev, control::IOCTL_REMOVE, bytemuck::bytes_of(&req), &mut none) }.map(|_| ()) + unsafe { + ioctl( + dev, + control::IOCTL_REMOVE, + bytemuck::bytes_of(&req), + &mut none, + ) + } + .map(|_| ()) } unsafe fn ping(&self, dev: HANDLE) -> Result<()> { diff --git a/crates/punktfunk-host/src/windows/win_display.rs b/crates/punktfunk-host/src/windows/win_display.rs index 0d51d62..ac34952 100644 --- a/crates/punktfunk-host/src/windows/win_display.rs +++ b/crates/punktfunk-host/src/windows/win_display.rs @@ -19,9 +19,9 @@ use windows::Win32::Devices::Display::{ QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, - DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, - SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, SDC_SAVE_TO_DATABASE, - SDC_USE_SUPPLIED_DISPLAY_CONFIG, + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME, + QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_FORCE_MODE_ENUMERATION, + SDC_SAVE_TO_DATABASE, SDC_USE_SUPPLIED_DISPLAY_CONFIG, }; use windows::Win32::Graphics::Gdi::{ ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW,