From 6ea036766ac344373b393a0f6b92c3016738c428 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 17 Jul 2026 09:00:53 +0200 Subject: [PATCH] refactor(host/W6.1): extract GPU vendor/adapter detection into the pf-gpu leaf crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth de-coupling for the host crate carve (plan §W6.1 leaf). gpu.rs (inventory, selection preference, active-session accounting — deps only pf-host-config + pf-paths, no subsystem refs) moves to a new pf-gpu leaf so pf-encode/pf-capture/pf-vdisplay can consult the selected GPU without an orchestrator edge. ~50 crate::gpu:: sites repoint to pf_gpu::; the ~30 pub(crate) items become pub (crate API). assign_ids gets a macOS-only allow(dead_code) (used only by the Linux/Windows enumerate arms). Verified: Linux (home-worker-5) clippy -p pf-gpu -p punktfunk-host --all-targets -D warnings + pf-gpu tests (12 pass); Windows (192.168.1.158) clippy --features nvenc,amf-qsv --all-targets green. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 15 +++++ Cargo.toml | 1 + crates/pf-gpu/Cargo.toml | 28 +++++++++ .../src/gpu.rs => pf-gpu/src/lib.rs} | 63 ++++++++++--------- crates/punktfunk-host/Cargo.toml | 2 + crates/punktfunk-host/src/encode.rs | 51 +++++++-------- .../punktfunk-host/src/encode/linux/vaapi.rs | 6 +- crates/punktfunk-host/src/main.rs | 1 - crates/punktfunk-host/src/mgmt/gpu.rs | 30 ++++----- crates/punktfunk-host/src/vdisplay/policy.rs | 4 +- .../src/vdisplay/windows/manager.rs | 2 +- .../punktfunk-host/src/windows/win_adapter.rs | 6 +- 12 files changed, 126 insertions(+), 83 deletions(-) create mode 100644 crates/pf-gpu/Cargo.toml rename crates/{punktfunk-host/src/gpu.rs => pf-gpu/src/lib.rs} (96%) diff --git a/Cargo.lock b/Cargo.lock index a2544b46..f38b8963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2808,6 +2808,20 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "pf-gpu" +version = "0.12.0" +dependencies = [ + "anyhow", + "pf-host-config", + "pf-paths", + "serde", + "serde_json", + "tempfile", + "tracing", + "windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "pf-host-config" version = "0.12.0" @@ -3133,6 +3147,7 @@ dependencies = [ "opus", "parking_lot", "pf-driver-proto", + "pf-gpu", "pf-host-config", "pf-paths", "pipewire", diff --git a/Cargo.toml b/Cargo.toml index 461f813e..6b6e87ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/pf-driver-proto", "crates/pf-paths", "crates/pf-host-config", + "crates/pf-gpu", "crates/pyrowave-sys", "clients/probe", "clients/linux", diff --git a/crates/pf-gpu/Cargo.toml b/crates/pf-gpu/Cargo.toml new file mode 100644 index 00000000..526c3dda --- /dev/null +++ b/crates/pf-gpu/Cargo.toml @@ -0,0 +1,28 @@ +# GPU vendor/adapter detection + selection + the live-session record, extracted into a leaf crate so +# every subsystem crate (pf-encode, pf-capture, pf-vdisplay) can consult the selected GPU WITHOUT +# depending on the orchestrator (plan §W6). Self-contained: reads config + writes the pref store. +[package] +name = "pf-gpu" +version = "0.12.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "punktfunk host GPU vendor/adapter enumeration, selection preference, and active-session accounting." +publish = false + +[dependencies] +pf-host-config = { path = "../pf-host-config" } +pf-paths = { path = "../pf-paths" } +anyhow = "1" +tracing = "0.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_Graphics_Dxgi", + "Win32_Graphics_Dxgi_Common", +] } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/punktfunk-host/src/gpu.rs b/crates/pf-gpu/src/lib.rs similarity index 96% rename from crates/punktfunk-host/src/gpu.rs rename to crates/pf-gpu/src/lib.rs index 683f4814..61090e82 100644 --- a/crates/punktfunk-host/src/gpu.rs +++ b/crates/pf-gpu/src/lib.rs @@ -28,14 +28,14 @@ use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; /// PCI vendor ids of the GPU vendors the encode backends know (NVENC / AMF / QSV, VAAPI on Linux). -pub(crate) const VENDOR_NVIDIA: u32 = 0x10DE; -pub(crate) const VENDOR_AMD: u32 = 0x1002; -pub(crate) const VENDOR_INTEL: u32 = 0x8086; +pub const VENDOR_NVIDIA: u32 = 0x10DE; +pub const VENDOR_AMD: u32 = 0x1002; +pub const VENDOR_INTEL: u32 = 0x8086; /// Platform handle of an enumerated GPU — how the pipeline actually addresses it. Not part of the /// stable identity (Windows LUIDs are per-boot; a render node can renumber across kernel updates). #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(crate) struct GpuHandle { +pub struct GpuHandle { /// DXGI `AdapterLuid` of this adapter (this boot only). #[cfg(target_os = "windows")] pub luid_low: u32, @@ -48,7 +48,7 @@ pub(crate) struct GpuHandle { /// One hardware GPU as enumerated on this host. #[derive(Clone, Debug)] -pub(crate) struct GpuInfo { +pub struct GpuInfo { /// Stable identifier for the API/UI: `"{vendor:04x}-{device:04x}-{occurrence}"`. Occurrence /// disambiguates identical cards (two of the same model) by enumeration order among their /// twins — the best available tiebreaker (PCI order), imperfect but honest. @@ -65,7 +65,7 @@ pub(crate) struct GpuInfo { } /// Lowercase vendor tag for the API (`nvidia` / `amd` / `intel` / `other`). -pub(crate) fn vendor_tag(vendor_id: u32) -> &'static str { +pub fn vendor_tag(vendor_id: u32) -> &'static str { match vendor_id { VENDOR_NVIDIA => "nvidia", VENDOR_AMD => "amd", @@ -93,6 +93,9 @@ impl GpuInfo { /// Assign the stable `id` + `occurrence` fields after enumeration (occurrence = index among /// same-(vendor,device) twins, in inventory order — Windows sorts the inventory by LUID first so /// twin numbering is stable for the boot, see [`enumerate`]). +// Called only by the Linux/Windows `enumerate()` arms; the stub `enumerate()` on other targets +// (macOS dev host) doesn't, so it's dead there. +#[cfg_attr(not(any(target_os = "linux", target_os = "windows")), allow(dead_code))] fn assign_ids(gpus: &mut [GpuInfo]) { for i in 0..gpus.len() { let occ = gpus[..i] @@ -127,7 +130,7 @@ mod adapter_type { /// True when these bits describe an adapter that can never be the render/encode GPU: /// indirect-display, software, or anything without render support. - pub(crate) fn hidden(bits: u32) -> bool { + pub fn hidden(bits: u32) -> bool { bits & INDIRECT_DISPLAY_DEVICE != 0 || bits & SOFTWARE_DEVICE != 0 || bits & RENDER_SUPPORTED == 0 @@ -172,7 +175,7 @@ mod kmt { /// The `D3DKMT_ADAPTERTYPE` bits for the adapter with this LUID, `None` when the kernel /// query fails (callers fail open — better a listed twin than a hidden real GPU). - pub(crate) fn adapter_type_bits(luid_low: u32, luid_high: i32) -> Option { + pub fn adapter_type_bits(luid_low: u32, luid_high: i32) -> Option { // SAFETY: every pointer handed to the three D3DKMT calls addresses a stack local that // outlives the call; NTSTATUS >= 0 is success. The kernel handle is closed on every // path that opened it, including a failed query. @@ -208,7 +211,7 @@ mod kmt { /// Other platforms (the macOS dev/test host build): empty — the endpoints still exist, they just /// report no GPUs. #[cfg(target_os = "windows")] -pub(crate) fn enumerate() -> Vec { +pub fn enumerate() -> Vec { use windows::Win32::Graphics::Dxgi::{ CreateDXGIFactory1, IDXGIFactory1, DXGI_ADAPTER_FLAG_SOFTWARE, }; @@ -281,7 +284,7 @@ pub(crate) fn enumerate() -> Vec { } #[cfg(target_os = "linux")] -pub(crate) fn enumerate() -> Vec { +pub fn enumerate() -> Vec { let mut nodes: Vec = std::fs::read_dir("/dev/dri") .map(|rd| { rd.filter_map(|e| e.ok()) @@ -331,7 +334,7 @@ pub(crate) fn enumerate() -> Vec { } #[cfg(not(any(target_os = "windows", target_os = "linux")))] -pub(crate) fn enumerate() -> Vec { +pub fn enumerate() -> Vec { Vec::new() } @@ -343,7 +346,7 @@ pub(crate) fn enumerate() -> Vec { /// `Manual` (an explicit GPU chosen in the web console). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub(crate) enum GpuMode { +pub enum GpuMode { #[default] Auto, Manual, @@ -351,7 +354,7 @@ pub(crate) enum GpuMode { /// Stable identity of the manually preferred GPU (see [`GpuInfo::id`] for why not LUID/index). #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct PreferredGpu { +pub struct PreferredGpu { pub vendor_id: u32, pub device_id: u32, #[serde(default)] @@ -364,7 +367,7 @@ pub(crate) struct PreferredGpu { /// The persisted GPU preference (`/gpu-settings.json`). #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct GpuPreference { +pub struct GpuPreference { #[serde(default)] pub mode: GpuMode, /// `Some` when `mode == Manual` (kept when switching back to Auto so the console can offer @@ -376,7 +379,7 @@ pub(crate) struct GpuPreference { /// The preference store: in-memory current value + its JSON file. Mirrors `native_pairing`'s /// persistence discipline (private dir, secret-file temp write + atomic rename, in-memory /// rollback if the disk write fails). -pub(crate) struct GpuPrefStore { +pub struct GpuPrefStore { path: PathBuf, cur: Mutex, } @@ -422,7 +425,7 @@ impl GpuPrefStore { /// The process-wide preference store (config-dir file), loaded once on first access — the same /// global-accessor shape as [`pf_host_config::config`], because selection happens deep inside /// capture/encode setup where no app state is threaded. -pub(crate) fn prefs() -> &'static GpuPrefStore { +pub fn prefs() -> &'static GpuPrefStore { static STORE: OnceLock = OnceLock::new(); STORE.get_or_init(|| GpuPrefStore::load_from(pf_paths::config_dir().join("gpu-settings.json"))) } @@ -433,7 +436,7 @@ pub(crate) fn prefs() -> &'static GpuPrefStore { /// Why a GPU was selected — surfaced by the mgmt API so the console can explain the decision. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum PickSource { +pub enum PickSource { /// The operator's manual preference matched a present GPU. Preference, /// `PUNKTFUNK_RENDER_ADAPTER` substring matched. @@ -458,7 +461,7 @@ impl PickSource { /// A resolved selection: the GPU the next session's pipeline will be created on, and why. #[derive(Clone, Debug)] -pub(crate) struct SelectedGpu { +pub struct SelectedGpu { pub info: GpuInfo, pub source: PickSource, } @@ -466,7 +469,7 @@ pub(crate) struct SelectedGpu { /// Find the manually preferred GPU in the inventory. Match order: exact stable identity /// (vendor, device, occurrence) → same model (vendor, device; a twin renumbered) → exact name /// (ids changed across a driver/firmware quirk but the marketing name survived). -pub(crate) fn find_preferred(gpus: &[GpuInfo], want: &PreferredGpu) -> Option { +pub fn find_preferred(gpus: &[GpuInfo], want: &PreferredGpu) -> Option { gpus.iter() .position(|g| { g.vendor_id == want.vendor_id @@ -489,7 +492,7 @@ pub(crate) fn find_preferred(gpus: &[GpuInfo], want: &PreferredGpu) -> Option, @@ -532,7 +535,7 @@ pub(crate) fn pick( /// the encoder-vendor dispatch both consume, so capture, encode, and the advertisement agree by /// construction. Pure query — callers log (this runs per serverinfo poll). #[cfg(target_os = "windows")] -pub(crate) fn selected_gpu() -> Option { +pub fn selected_gpu() -> Option { let gpus = enumerate(); let pref = prefs().get(); let env = pf_host_config::config() @@ -551,7 +554,7 @@ pub(crate) fn selected_gpu() -> Option { /// owns the VAAPI render node. (The *authoritative* Linux switches stay in `encode::open_video` / /// [`linux_render_node`] — this is the console's view of them.) #[cfg(target_os = "linux")] -pub(crate) fn selected_gpu() -> Option { +pub fn selected_gpu() -> Option { let gpus = enumerate(); let pref = prefs().get(); let mut preference_missing = false; @@ -593,14 +596,14 @@ pub(crate) fn selected_gpu() -> Option { } #[cfg(not(any(target_os = "windows", target_os = "linux")))] -pub(crate) fn selected_gpu() -> Option { +pub fn selected_gpu() -> Option { None } /// The manually preferred GPU, only when `mode == Manual` **and** it is currently present. /// The Linux encode dispatch consults this (auto mode keeps today's NVIDIA-presence behavior /// exactly). -pub(crate) fn manual_selection() -> Option { +pub fn manual_selection() -> Option { let pref = prefs().get(); if pref.mode != GpuMode::Manual { return None; @@ -614,7 +617,7 @@ pub(crate) fn manual_selection() -> Option { /// The VAAPI/DRM render node for this host: matched manual preference > `PUNKTFUNK_RENDER_NODE` /// (a deliberate live env read — see `config.rs` module docs) > `/dev/dri/renderD128`. #[cfg(target_os = "linux")] -pub(crate) fn linux_render_node() -> PathBuf { +pub fn linux_render_node() -> PathBuf { if let Some(g) = manual_selection() { if let Some(node) = g.handle.render_node { return node; @@ -637,7 +640,7 @@ fn linux_nvidia_present() -> bool { /// A cache key that changes whenever the *selection* changes (preference edits included), for the /// per-GPU probe caches (`can_encode_444`, `windows_codec_support`) that were process-lifetime /// `OnceLock`s back when selection was env-only. -pub(crate) fn selection_key() -> String { +pub fn selection_key() -> String { match selected_gpu() { Some(sel) => { #[cfg(target_os = "windows")] @@ -662,7 +665,7 @@ pub(crate) fn selection_key() -> String { /// What a live session encodes on — the console's "currently used GPU". #[derive(Clone, Debug)] -pub(crate) struct ActiveGpu { +pub struct ActiveGpu { /// Stable id of the GPU ([`GpuInfo::id`]; empty for the CPU/software path) so a UI can match /// it against the inventory. pub id: String, @@ -682,7 +685,7 @@ static ACTIVE: Mutex> = Mutex::new(None); /// RAII marker for one live encode session; dropping it decrements the session count. Held by the /// encoder wrapper `open_video` returns, so the count is correct by construction (every successful /// open is paired with a drop). -pub(crate) struct ActiveSession(()); +pub struct ActiveSession(()); impl Drop for ActiveSession { fn drop(&mut self) { @@ -696,7 +699,7 @@ impl Drop for ActiveSession { /// Record a session opening on `gpu`. Concurrent sessions share one GPU (the Windows pipeline is /// single-GPU by construction; Linux sessions share the selection), so the latest record wins and /// a counter tracks liveness. -pub(crate) fn session_begin(gpu: ActiveGpu) -> ActiveSession { +pub fn session_begin(gpu: ActiveGpu) -> ActiveSession { let mut st = ACTIVE.lock().unwrap_or_else(|e| e.into_inner()); let sessions = st.as_ref().map(|s| s.sessions).unwrap_or(0) + 1; *st = Some(ActiveState { gpu, sessions }); @@ -705,7 +708,7 @@ pub(crate) fn session_begin(gpu: ActiveGpu) -> ActiveSession { /// The GPU live sessions encode on + how many sessions hold it. `Some` with `sessions == 0` means /// "last used, idle now" — the mgmt API distinguishes the two. -pub(crate) fn active() -> Option<(ActiveGpu, u32)> { +pub fn active() -> Option<(ActiveGpu, u32)> { ACTIVE .lock() .unwrap_or_else(|e| e.into_inner()) diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 3b2001ee..7f05b703 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -14,6 +14,8 @@ punktfunk-core = { path = "../punktfunk-core", features = ["quic"] } pf-paths = { path = "../pf-paths" } # Process-wide host config global, extracted to a leaf crate (plan §W6). pf-host-config = { path = "../pf-host-config" } +# GPU vendor/adapter detection + selection, extracted to a leaf crate (plan §W6). +pf-gpu = { path = "../pf-gpu" } # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP). quinn = "0.11" anyhow = "1" diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index c4eaddf9..70b3f18a 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -128,24 +128,24 @@ pub fn open_video( // mirroring its dispatch, which went stale the moment a backend gained an internal fallback // (the default-on Vulkan Video path falls back to VAAPI on a failed open, and a dispatch // mirror would report "vaapi" for every Vulkan session or vice versa). The GPU identity is the - // same selection the capturer was created on ([`crate::gpu::selected_gpu`]). Dropping the + // same selection the capturer was created on ([`pf_gpu::selected_gpu`]). Dropping the // returned encoder ends the record, so the live count is correct by construction. let gpu = if backend == "software" { - crate::gpu::ActiveGpu { + pf_gpu::ActiveGpu { id: String::new(), name: "CPU (openh264)".into(), vendor_id: 0, backend, } } else { - match crate::gpu::selected_gpu() { - Some(sel) => crate::gpu::ActiveGpu { + match pf_gpu::selected_gpu() { + Some(sel) => pf_gpu::ActiveGpu { id: sel.info.id, name: sel.info.name, vendor_id: sel.info.vendor_id, backend, }, - None => crate::gpu::ActiveGpu { + None => pf_gpu::ActiveGpu { id: String::new(), name: "GPU".into(), vendor_id: 0, @@ -155,7 +155,7 @@ pub fn open_video( }; Ok(Box::new(TrackedEncoder { inner, - _session: crate::gpu::session_begin(gpu), + _session: pf_gpu::session_begin(gpu), })) } @@ -163,7 +163,7 @@ pub fn open_video( /// otherwise. struct TrackedEncoder { inner: Box, - _session: crate::gpu::ActiveSession, + _session: pf_gpu::ActiveSession, } impl Encoder for TrackedEncoder { @@ -406,11 +406,11 @@ fn open_video_backend( // explicit PUNKTFUNK_ENCODER contradicts the GPU the pipeline sits on (e.g. `nvenc` forced // while the web-console preference pins the Intel iGPU) — the open below will then fail on // a wrong-vendor device; say why up front instead of leaving an opaque encoder error. - if let Some(sel) = crate::gpu::selected_gpu() { + if let Some(sel) = pf_gpu::selected_gpu() { let mismatched = match backend { - WindowsBackend::Nvenc => sel.info.vendor_id != crate::gpu::VENDOR_NVIDIA, - WindowsBackend::Amf => sel.info.vendor_id != crate::gpu::VENDOR_AMD, - WindowsBackend::Qsv => sel.info.vendor_id != crate::gpu::VENDOR_INTEL, + WindowsBackend::Nvenc => sel.info.vendor_id != pf_gpu::VENDOR_NVIDIA, + WindowsBackend::Amf => sel.info.vendor_id != pf_gpu::VENDOR_AMD, + WindowsBackend::Qsv => sel.info.vendor_id != pf_gpu::VENDOR_INTEL, WindowsBackend::Software => false, }; if mismatched { @@ -680,14 +680,14 @@ fn nvidia_present() -> bool { } /// The `auto` Linux backend decision, shared by [`open_video`] and [`linux_zero_copy_is_vaapi`]: -/// a manual web-console GPU preference (when that GPU is present — [`crate::gpu::manual_selection`]) +/// a manual web-console GPU preference (when that GPU is present — [`pf_gpu::manual_selection`]) /// picks its vendor's backend — AMD/Intel → VAAPI on that GPU's render node, NVIDIA → NVENC (still /// requiring the proprietary driver's device nodes; a nouveau NVIDIA GPU can't NVENC) — otherwise /// today's NVIDIA-presence probe, unchanged. #[cfg(target_os = "linux")] fn linux_auto_is_vaapi() -> bool { - if let Some(g) = crate::gpu::manual_selection() { - if g.vendor_id == crate::gpu::VENDOR_NVIDIA { + if let Some(g) = pf_gpu::manual_selection() { + if g.vendor_id == pf_gpu::VENDOR_NVIDIA { return !nvidia_present(); } return true; @@ -788,7 +788,7 @@ pub fn can_encode_444(codec: Codec) -> bool { // Cached per selected GPU (was a process-lifetime OnceLock): a web-console preference change // re-probes on the newly selected adapter before the next Welcome. static CACHE: OnceLock>> = OnceLock::new(); - let key = crate::gpu::selection_key(); + let key = pf_gpu::selection_key(); let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); if let Some(v) = cache.lock().unwrap().get(&key) { return *v; @@ -870,7 +870,7 @@ pub fn can_encode_10bit(codec: Codec) -> bool { // Cached per (selected GPU, codec) — a web-console preference change re-probes on the newly // selected adapter before the next Welcome, mirroring `can_encode_444`. static CACHE: OnceLock>> = OnceLock::new(); - let key = (crate::gpu::selection_key(), codec.label()); + let key = (pf_gpu::selection_key(), codec.label()); let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); if let Some(v) = cache.lock().unwrap().get(&key) { return *v; @@ -1007,7 +1007,7 @@ pub fn windows_backend_is_probed() -> bool { } } -/// Detect the encode-GPU vendor from the **selected render adapter** ([`crate::gpu::selected_gpu`]: +/// Detect the encode-GPU vendor from the **selected render adapter** ([`pf_gpu::selected_gpu`]: /// web-console preference > `PUNKTFUNK_RENDER_ADAPTER` > max VRAM) — the same adapter the capture /// ring and the IddCx render pin sit on, so the encoder backend can never disagree with where the /// captured frames live. The old first-DXGI-adapter scan did exactly that on hybrid boxes: adapter @@ -1019,18 +1019,15 @@ pub fn windows_backend_is_probed() -> bool { fn windows_gpu_vendor() -> Option { fn by_id(vendor_id: u32) -> Option { match vendor_id { - crate::gpu::VENDOR_NVIDIA => Some(GpuVendor::Nvidia), - crate::gpu::VENDOR_AMD => Some(GpuVendor::Amd), - crate::gpu::VENDOR_INTEL => Some(GpuVendor::Intel), + pf_gpu::VENDOR_NVIDIA => Some(GpuVendor::Nvidia), + pf_gpu::VENDOR_AMD => Some(GpuVendor::Amd), + pf_gpu::VENDOR_INTEL => Some(GpuVendor::Intel), _ => None, } } - let sel = crate::gpu::selected_gpu()?; - by_id(sel.info.vendor_id).or_else(|| { - crate::gpu::enumerate() - .iter() - .find_map(|g| by_id(g.vendor_id)) - }) + let sel = pf_gpu::selected_gpu()?; + by_id(sel.info.vendor_id) + .or_else(|| pf_gpu::enumerate().iter().find_map(|g| by_id(g.vendor_id))) } /// Probe the active Windows AMF/QSV backend for its encodable codecs (cached **per (backend, @@ -1050,7 +1047,7 @@ pub fn windows_codec_support() -> CodecSupport { use std::sync::{Mutex, OnceLock}; static CACHE: OnceLock>> = OnceLock::new(); let backend = windows_resolved_backend(); - let key = format!("{backend:?}:{}", crate::gpu::selection_key()); + let key = format!("{backend:?}:{}", pf_gpu::selection_key()); let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); if let Some(c) = cache.lock().unwrap().get(&key) { return *c; diff --git a/crates/punktfunk-host/src/encode/linux/vaapi.rs b/crates/punktfunk-host/src/encode/linux/vaapi.rs index 09b1a1a5..0feee5d5 100644 --- a/crates/punktfunk-host/src/encode/linux/vaapi.rs +++ b/crates/punktfunk-host/src/encode/linux/vaapi.rs @@ -44,13 +44,11 @@ const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 { (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) } -/// The render node a VAAPI/DRM device should open, from [`crate::gpu::linux_render_node`]: a +/// The render node a VAAPI/DRM device should open, from [`pf_gpu::linux_render_node`]: a /// matched web-console GPU preference pins it, else `PUNKTFUNK_RENDER_NODE`, else the single-GPU /// default. fn render_node() -> CString { - let p = crate::gpu::linux_render_node() - .to_string_lossy() - .into_owned(); + let p = pf_gpu::linux_render_node().to_string_lossy().into_owned(); CString::new(p).unwrap_or_else(|_| CString::new("/dev/dri/renderD128").unwrap()) } diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 24869bf6..5cf4bc3a 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -44,7 +44,6 @@ mod drm_sync; mod encode; mod events; mod gamestream; -mod gpu; #[cfg(target_os = "linux")] #[path = "linux/gpuclocks.rs"] mod gpuclocks; diff --git a/crates/punktfunk-host/src/mgmt/gpu.rs b/crates/punktfunk-host/src/mgmt/gpu.rs index 972e4d6f..fdeff98e 100644 --- a/crates/punktfunk-host/src/mgmt/gpu.rs +++ b/crates/punktfunk-host/src/mgmt/gpu.rs @@ -86,11 +86,11 @@ pub(crate) struct SetGpuPreference { /// Build the [`GpuState`] snapshot (shared by the GET and the PUT's response). pub(crate) fn gpu_state() -> GpuState { - let gpus = crate::gpu::enumerate(); - let pref = crate::gpu::prefs().get(); + let gpus = pf_gpu::enumerate(); + let pref = pf_gpu::prefs().get(); let (preferred_id, preferred_name, preferred_available) = match &pref.gpu { Some(want) => { - let found = crate::gpu::find_preferred(&gpus, want); + let found = pf_gpu::find_preferred(&gpus, want); let id = match found { // Canonical: the present GPU's id (identity may have matched loosely). Some(i) => gpus[i].id.clone(), @@ -107,15 +107,15 @@ pub(crate) fn gpu_state() -> GpuState { } None => (None, None, false), }; - let selected = crate::gpu::selected_gpu().map(|sel| ApiSelectedGpu { + let selected = pf_gpu::selected_gpu().map(|sel| ApiSelectedGpu { vendor: sel.info.vendor_tag().into(), id: sel.info.id, name: sel.info.name, source: sel.source.tag().into(), }); - let active = crate::gpu::active().and_then(|(g, sessions)| { + let active = pf_gpu::active().and_then(|(g, sessions)| { (sessions > 0).then(|| ApiActiveGpu { - vendor: crate::gpu::vendor_tag(g.vendor_id).into(), + vendor: pf_gpu::vendor_tag(g.vendor_id).into(), id: g.id, name: g.name, backend: g.backend.into(), @@ -133,8 +133,8 @@ pub(crate) fn gpu_state() -> GpuState { }) .collect(), mode: match pref.mode { - crate::gpu::GpuMode::Auto => "auto".into(), - crate::gpu::GpuMode::Manual => "manual".into(), + pf_gpu::GpuMode::Auto => "auto".into(), + pf_gpu::GpuMode::Manual => "manual".into(), }, preferred_id, preferred_name, @@ -189,8 +189,8 @@ pub(crate) async fn set_gpu_preference(ApiJson(req): ApiJson) let pref = match req.mode.to_ascii_lowercase().as_str() { "auto" => { // Keep the stored manual pick so the console can offer switching back to it. - let mut p = crate::gpu::prefs().get(); - p.mode = crate::gpu::GpuMode::Auto; + let mut p = pf_gpu::prefs().get(); + p.mode = pf_gpu::GpuMode::Auto; p } "manual" => { @@ -202,15 +202,15 @@ pub(crate) async fn set_gpu_preference(ApiJson(req): ApiJson) else { return api_error(StatusCode::BAD_REQUEST, "mode `manual` requires `gpu_id`"); }; - let Some(g) = crate::gpu::enumerate().into_iter().find(|g| g.id == id) else { + let Some(g) = pf_gpu::enumerate().into_iter().find(|g| g.id == id) else { return api_error( StatusCode::BAD_REQUEST, "gpu_id does not match a present GPU (see GET /gpus)", ); }; - crate::gpu::GpuPreference { - mode: crate::gpu::GpuMode::Manual, - gpu: Some(crate::gpu::PreferredGpu { + pf_gpu::GpuPreference { + mode: pf_gpu::GpuMode::Manual, + gpu: Some(pf_gpu::PreferredGpu { vendor_id: g.vendor_id, device_id: g.device_id, occurrence: g.occurrence, @@ -225,7 +225,7 @@ pub(crate) async fn set_gpu_preference(ApiJson(req): ApiJson) ) } }; - if let Err(e) = crate::gpu::prefs().set(pref) { + if let Err(e) = pf_gpu::prefs().set(pref) { return api_error( StatusCode::INTERNAL_SERVER_ERROR, &format!("persist GPU preference: {e:#}"), diff --git a/crates/punktfunk-host/src/vdisplay/policy.rs b/crates/punktfunk-host/src/vdisplay/policy.rs index 6a0ab127..f4d45bde 100644 --- a/crates/punktfunk-host/src/vdisplay/policy.rs +++ b/crates/punktfunk-host/src/vdisplay/policy.rs @@ -408,7 +408,7 @@ pub fn preset_fields(preset: Preset) -> Option { } /// The persisted policy store: the loaded file value (or `None` when no file exists) behind its -/// JSON path. Mirrors [`crate::gpu::GpuPrefStore`] — private dir, temp-write + atomic rename, +/// JSON path. Mirrors [`pf_gpu::GpuPrefStore`] — private dir, temp-write + atomic rename, /// in-memory rollback if the disk write fails. pub struct DisplayPolicyStore { path: PathBuf, @@ -492,7 +492,7 @@ impl DisplayPolicyStore { } /// The process-wide display-policy store (config-dir file), loaded once on first access — the same -/// global-accessor shape as [`crate::gpu::prefs`], because display setup happens deep in the +/// global-accessor shape as [`pf_gpu::prefs`], because display setup happens deep in the /// capture/vdisplay path where no app state is threaded. pub fn prefs() -> &'static DisplayPolicyStore { static STORE: OnceLock = OnceLock::new(); diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index 9772d623..98aa392c 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -1462,7 +1462,7 @@ fn resolve_render_pin() -> Option { /// not be compared with render-GPU picks. fn warn_if_pick_moved(mon: &Monitor) { let Some(pin) = mon.render_pin else { return }; - let Some(sel) = crate::gpu::selected_gpu() else { + let Some(sel) = pf_gpu::selected_gpu() else { return; }; let pick = sel.info.luid(); diff --git a/crates/punktfunk-host/src/windows/win_adapter.rs b/crates/punktfunk-host/src/windows/win_adapter.rs index 86a3eb46..f5a03adc 100644 --- a/crates/punktfunk-host/src/windows/win_adapter.rs +++ b/crates/punktfunk-host/src/windows/win_adapter.rs @@ -16,11 +16,11 @@ use windows::Win32::Foundation::LUID; /// Pick the render GPU LUID the 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 [`crate::gpu::selected_gpu`] for the precedence. A configured +/// follow this one decision — see [`pf_gpu::selected_gpu`] for the precedence. 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. pub(crate) fn resolve_render_adapter_luid() -> Option { - match crate::gpu::selected_gpu() { + match pf_gpu::selected_gpu() { Some(sel) => { tracing::info!( adapter = sel.info.name, @@ -28,7 +28,7 @@ pub(crate) fn resolve_render_adapter_luid() -> Option { source = sel.source.tag(), "render adapter selected" ); - if sel.source == crate::gpu::PickSource::PreferenceMissing { + if sel.source == pf_gpu::PickSource::PreferenceMissing { tracing::warn!( "the preferred GPU is not present — auto-selected the adapter above \ (fix or clear the preference in the web console)"