diff --git a/clients/windows/src/app/mod.rs b/clients/windows/src/app/mod.rs index 0df8bf5f..e0e8c07f 100644 --- a/clients/windows/src/app/mod.rs +++ b/clients/windows/src/app/mod.rs @@ -195,6 +195,9 @@ fn apply_window_icon_when_ready() { }; let _ = std::thread::Builder::new() .name("pf-window-icon".into()) + // SAFETY: every call in this thread is a Win32 window/icon API taking either a static wide + // literal, a handle it just obtained and checked, or the module handle of this process; none + // of them dereference caller memory, and the loop gives up after 100 tries. .spawn(|| unsafe { for _ in 0..100 { if let Ok(hwnd) = FindWindowW(None, windows::core::w!("Punktfunk")) { diff --git a/clients/windows/src/gpu.rs b/clients/windows/src/gpu.rs index 2e9e306f..0cb957ca 100644 --- a/clients/windows/src/gpu.rs +++ b/clients/windows/src/gpu.rs @@ -10,6 +10,8 @@ use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIFact /// The adapter's human-readable description. fn adapter_name(adapter: &IDXGIAdapter) -> String { + // SAFETY: a read-only COM call on the live `adapter` borrow, filling a descriptor returned by + // value; `&IDXGIAdapter` is a reference-counted wrapper, so the borrow IS the liveness. unsafe { adapter .GetDesc() @@ -24,12 +26,16 @@ fn adapter_name(adapter: &IDXGIAdapter) -> String { /// Every DXGI adapter, in enumeration order. fn all_adapters() -> Vec { + // SAFETY: DXGI factory creation takes no pointer and returns an owned factory or an error, + // matched on below before anything uses it. let factory: IDXGIFactory1 = match unsafe { CreateDXGIFactory1() } { Ok(f) => f, Err(_) => return Vec::new(), }; let mut v = Vec::new(); let mut i = 0u32; + // SAFETY: a COM call on the live factory above; it takes an index and yields an owned adapter, + // and the `Ok` pattern is what proves one came back. while let Ok(a) = unsafe { factory.EnumAdapters1(i) } { i += 1; if let Ok(a) = a.cast::() { @@ -54,6 +60,7 @@ pub fn adapter_names() -> Vec { for a in all_adapters() { let desc1 = a .cast::() + // SAFETY: a read-only COM call on the adapter just cast, filling a descriptor by value. .and_then(|a1| unsafe { a1.GetDesc1() }) .ok(); let name = adapter_name(&a); diff --git a/clients/windows/src/main.rs b/clients/windows/src/main.rs index 37f67177..e740f00f 100644 --- a/clients/windows/src/main.rs +++ b/clients/windows/src/main.rs @@ -14,6 +14,8 @@ //! punktfunk-client --headless --speed-test --connect host[:port] //! (measure the path: probe burst → goodput / loss / recommended bitrate) +// Unsafe-proof program: every `unsafe {}` in this client carries a `// SAFETY:` proof. +#![deny(clippy::undocumented_unsafe_blocks)] // Link as a GUI (windows) subsystem binary so the default windowed launch (MSIX / double-click) // does NOT pop a console window. The CLI paths (--headless/--discover) reattach to the launching // terminal's console at startup (see main), so their output is still visible when run from a shell. @@ -49,6 +51,9 @@ fn main() { // launch is window-free. AttachConsole only binds to an ALREADY-EXISTING parent console (it // never creates one), so when launched from a terminal — `--headless`/`--discover` — stdout and // the tracing writer below land in that terminal; from Explorer/MSIX it's a harmless no-op. + // SAFETY: `AttachConsole` takes a plain process-id constant and binds to an already-existing + // parent console; it allocates nothing and dereferences no caller memory, and failure (no parent + // console) is ignored by design. unsafe { use windows::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS}; let _ = AttachConsole(ATTACH_PARENT_PROCESS); @@ -170,6 +175,9 @@ fn set_app_user_model_id() { use windows::Win32::Foundation::APPMODEL_ERROR_NO_PACKAGE; use windows::Win32::Storage::Packaging::Appx::GetCurrentPackageFullName; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; + // SAFETY: `GetCurrentPackageFullName` is called with `len = 0` and no buffer, which is the + // documented identity PROBE — it writes nothing and only reports whether this process is + // packaged; `SetCurrentProcessExplicitAppUserModelID` takes a static wide literal. unsafe { let mut len: u32 = 0; // No buffer: just probe whether the process has package identity. diff --git a/clients/windows/src/shell_window.rs b/clients/windows/src/shell_window.rs index e7b18578..24c528a2 100644 --- a/clients/windows/src/shell_window.rs +++ b/clients/windows/src/shell_window.rs @@ -18,6 +18,9 @@ use windows::Win32::UI::WindowsAndMessaging::{ static SHELL_HWND: AtomicIsize = AtomicIsize::new(0); fn shell_hwnd() -> Option { + // SAFETY: the cached value is an `HWND` this process obtained itself; it is re-validated with + // `IsWindow` before use (a stale handle is treated as absent), and the lookup calls take only + // static wide literals. unsafe { let cached = SHELL_HWND.load(Ordering::Relaxed); if cached != 0 { @@ -37,6 +40,8 @@ fn shell_hwnd() -> Option { /// the shell in view with its error banner). pub(crate) fn hide() { if let Some(h) = shell_hwnd() { + // SAFETY: `h` is the validated shell window handle from `shell_hwnd`; `ShowWindow` takes it + // plus a plain flag and dereferences nothing. unsafe { let _ = ShowWindow(h, SW_HIDE); } @@ -47,6 +52,8 @@ pub(crate) fn hide() { /// it was never hidden — showing a visible window is a no-op. pub(crate) fn restore() { if let Some(h) = shell_hwnd() { + // SAFETY: as `hide` — `h` is the validated shell window handle, and both calls take only + // that handle plus a plain flag. unsafe { let _ = ShowWindow(h, SW_SHOW); let _ = SetForegroundWindow(h); @@ -60,6 +67,7 @@ pub(crate) fn restore() { pub(crate) fn position() -> Option<(i32, i32)> { let h = shell_hwnd()?; let mut r = RECT::default(); + // SAFETY: `h` is the validated shell window handle and `r` is a live local the call fills. unsafe { GetWindowRect(h, &mut r).ok()? }; Some((r.left, r.top)) } diff --git a/crates/pf-console-ui/src/lib.rs b/crates/pf-console-ui/src/lib.rs index f6c684ac..4194b2b0 100644 --- a/crates/pf-console-ui/src/lib.rs +++ b/crates/pf-console-ui/src/lib.rs @@ -10,6 +10,9 @@ //! own keyboard types through SDL text input) — plus the in-stream chrome: stats OSD, //! capture hint, start banner. +// Unsafe-proof program: every `unsafe {}` in the Skia/Vulkan overlay carries a `// SAFETY:` proof. +#![deny(clippy::undocumented_unsafe_blocks)] + #[cfg(any(target_os = "linux", windows))] mod anim; #[cfg(any(target_os = "linux", windows))] diff --git a/crates/pf-console-ui/src/shell.rs b/crates/pf-console-ui/src/shell.rs index 781081f8..e49d0a47 100644 --- a/crates/pf-console-ui/src/shell.rs +++ b/crates/pf-console-ui/src/shell.rs @@ -427,6 +427,9 @@ impl Shell { fn draw_aurora(&self, canvas: &Canvas, w: f64, h: f64, t: f64) { let uniforms: [f32; 3] = [w as f32, h as f32, t as f32]; + // SAFETY: `uniforms` is a local `[f32; 3]` — exactly 12 bytes — and `f32` has no padding or + // invalid bit patterns, so reading it as bytes is sound; the slice is copied by + // `Data::new_copy` before `uniforms` goes out of scope. let bytes = unsafe { std::slice::from_raw_parts(uniforms.as_ptr().cast::(), 12) }; match self.mesh.make_shader(Data::new_copy(bytes), &[], None) { Some(shader) => { diff --git a/crates/pf-console-ui/src/skia_overlay.rs b/crates/pf-console-ui/src/skia_overlay.rs index 33b6060e..45e17b3a 100644 --- a/crates/pf-console-ui/src/skia_overlay.rs +++ b/crates/pf-console-ui/src/skia_overlay.rs @@ -190,6 +190,9 @@ impl Drop for SkiaOverlay { fn drop(&mut self) { if let Some(gpu) = &mut self.gpu { for slot in self.slots.iter_mut().flat_map(Option::take) { + // SAFETY: the view belongs to this slot and the overlay is being dropped, so no + // further recording can reference it; the flush/submit + queue guard below is what + // retires any work that still could. unsafe { gpu.device.destroy_image_view(slot.view, None) }; drop(slot.surface); } @@ -208,6 +211,10 @@ impl Overlay for SkiaOverlay { let entry = shared.entry.clone(); let instance = shared.instance.clone(); let get_proc = move |of: skvk::GetProcOf| -> *const std::ffi::c_void { + // SAFETY: Skia calls this loader with raw instance/device handles it received from the + // `BackendContext` below — i.e. the very ones owned by `shared`, still live for the + // overlay's lifetime — and `from_raw` only rewraps them for the ash entry points. Each + // name is a NUL-terminated C string from Skia, borrowed for the call. unsafe { match of { skvk::GetProcOf::Instance(raw_instance, name) => entry @@ -223,6 +230,9 @@ impl Overlay for SkiaOverlay { } } }; + // SAFETY: the instance/physical-device/device handles come from `shared`, which owns them + // and outlives this backend context, and `get_proc` above resolves through those same + // handles. Skia stores them but does not take ownership — teardown stays ours. let backend = unsafe { skvk::BackendContext::new( shared.instance.handle().as_raw() as _, @@ -525,6 +535,9 @@ impl SkiaOverlay { if let Some(old) = self.slots[i].take() { // Any in-flight sampling of THIS slot ended two presents ago (the ring // alternates and the presenter waits its fence before each record). + // SAFETY: the view belongs to the slot being replaced, and per the comment above any + // in-flight sampling of THIS slot ended two presents ago — the ring alternates and the + // presenter waits its fence before each record — so the GPU is done with it. unsafe { gpu.device.destroy_image_view(old.view, None) }; } let info = @@ -549,6 +562,9 @@ impl SkiaOverlay { .vulkan_image_info() .context("backend texture is not Vulkan")?; let image = avk::Image::from_raw(*image_info.image() as u64); + // SAFETY: a create call on the live device `gpu` owns, over a builder that is a local + // outliving the call; `image` is the VkImage Skia just reported for this backend texture, + // which the surface keeps alive. The returned view is owned by the slot stored below. let view = unsafe { gpu.device.create_image_view( &avk::ImageViewCreateInfo::default() diff --git a/crates/pf-gpu/src/lib.rs b/crates/pf-gpu/src/lib.rs index b663482c..5364795b 100644 --- a/crates/pf-gpu/src/lib.rs +++ b/crates/pf-gpu/src/lib.rs @@ -22,6 +22,9 @@ //! running session keeps the device it opened on. [`session_begin`]/[`active`] record which GPU a //! live session actually encodes on, for the console's "in use" display. +// Unsafe-proof program: every `unsafe {}` in this leaf carries a `// SAFETY:` proof. +#![deny(clippy::undocumented_unsafe_blocks)] + use anyhow::Result; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -716,6 +719,42 @@ pub fn active() -> Option<(ActiveGpu, u32)> { .map(|s| (s.gpu.clone(), s.sessions)) } +/// Pick the render GPU LUID the Windows pipeline is created on: the IDD-push capturer's +/// shared-texture ring, the IddCx `SET_RENDER_ADAPTER` pin, and (via the captured frame's device) +/// NVENC/AMF/QSV all follow this one decision — see [`selected_gpu`] for the precedence (operator +/// preference > `PUNKTFUNK_RENDER_ADAPTER` substring > max `DedicatedVideoMemory`). A configured +/// preference that doesn't match a present GPU falls back to auto selection (with a warning) rather +/// than returning `None`, so a stale preference never stops the host from streaming. +/// +/// Lives here (not in a host module) so BOTH the capture and encode subsystem crates depend on it +/// as a peer of GPU selection instead of the orchestrator — the plan's `windows/adapter.rs`, folded +/// into `pf-gpu` (plan §W6). It was historically the SudoVDA backend's, then the host's +/// `win_adapter.rs`; the LUID-shaped view of [`selected_gpu`] plus the per-decision logging. +#[cfg(target_os = "windows")] +pub fn resolve_render_adapter_luid() -> Option { + match selected_gpu() { + Some(sel) => { + tracing::info!( + adapter = sel.info.name, + vram_mb = sel.info.vram_bytes / (1024 * 1024), + source = sel.source.tag(), + "render adapter selected" + ); + if sel.source == PickSource::PreferenceMissing { + tracing::warn!( + "the preferred GPU is not present — auto-selected the adapter above \ + (fix or clear the preference in the web console)" + ); + } + Some(sel.info.luid()) + } + None => { + tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER"); + None + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -914,39 +953,3 @@ mod tests { } } } - -/// Pick the render GPU LUID the Windows pipeline is created on: the IDD-push capturer's -/// shared-texture ring, the IddCx `SET_RENDER_ADAPTER` pin, and (via the captured frame's device) -/// NVENC/AMF/QSV all follow this one decision — see [`selected_gpu`] for the precedence (operator -/// preference > `PUNKTFUNK_RENDER_ADAPTER` substring > max `DedicatedVideoMemory`). A configured -/// preference that doesn't match a present GPU falls back to auto selection (with a warning) rather -/// than returning `None`, so a stale preference never stops the host from streaming. -/// -/// Lives here (not in a host module) so BOTH the capture and encode subsystem crates depend on it -/// as a peer of GPU selection instead of the orchestrator — the plan's `windows/adapter.rs`, folded -/// into `pf-gpu` (plan §W6). It was historically the SudoVDA backend's, then the host's -/// `win_adapter.rs`; the LUID-shaped view of [`selected_gpu`] plus the per-decision logging. -#[cfg(target_os = "windows")] -pub fn resolve_render_adapter_luid() -> Option { - match selected_gpu() { - Some(sel) => { - tracing::info!( - adapter = sel.info.name, - vram_mb = sel.info.vram_bytes / (1024 * 1024), - source = sel.source.tag(), - "render adapter selected" - ); - if sel.source == PickSource::PreferenceMissing { - tracing::warn!( - "the preferred GPU is not present — auto-selected the adapter above \ - (fix or clear the preference in the web console)" - ); - } - Some(sel.info.luid()) - } - None => { - tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER"); - None - } - } -} diff --git a/crates/punktfunk-tray/src/main.rs b/crates/punktfunk-tray/src/main.rs index 3639db2f..125a536f 100644 --- a/crates/punktfunk-tray/src/main.rs +++ b/crates/punktfunk-tray/src/main.rs @@ -10,6 +10,8 @@ //! then the host's loopback-only unauthenticated `GET /api/v1/local/summary` for the streaming //! details. Windows-subsystem binary — a console exe in the HKLM Run key would flash a terminal //! window at every sign-in. +// Unsafe-proof program: every `unsafe {}` in the tray carries a `// SAFETY:` proof. +#![deny(clippy::undocumented_unsafe_blocks)] #![cfg_attr(windows, windows_subsystem = "windows")] #[cfg(target_os = "linux")] diff --git a/tools/display-disturb/src/main.rs b/tools/display-disturb/src/main.rs index 79346e68..83556808 100644 --- a/tools/display-disturb/src/main.rs +++ b/tools/display-disturb/src/main.rs @@ -19,6 +19,9 @@ //! Usage: `display-disturb ddc [--interval-ms 2000] [--caps] [--vcp 0x10]` //! `display-disturb modeset [--interval-ms 2000]` +// Unsafe-proof program: every `unsafe {}` in this tool carries a `// SAFETY:` proof. +#![deny(clippy::undocumented_unsafe_blocks)] + #[cfg(not(target_os = "windows"))] fn main() { eprintln!("display-disturb is Windows-only (it exercises the WDDM display stack).");