From 6c2c8b6eabbd83aa2748e498b58211e30128195c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 26 Jul 2026 14:12:18 +0200 Subject: [PATCH] feat(mgmt/web): surface the host.env encoder pin so a conflicting GPU choice isn't invisible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit listGpus gains encoder_pin (PUNKTFUNK_ENCODER when it actually pins something — unset/auto stay null), and the console's GPU card shows it: a muted note when the pin is compatible, an amber warning naming the pinned vendor and the next session's GPU when it contradicts the selection (the host overrides such a pin at session open — without this field the selection just looked broken). Docs updated to say the adapter wins. Co-Authored-By: Claude Fable 5 --- api/openapi.json | 9 ++++- crates/punktfunk-host/src/mgmt/gpu.rs | 30 +++++++++++++++++ crates/punktfunk-host/src/mgmt/tests.rs | 7 ++++ docs-site/content/docs/configuration.md | 2 +- docs-site/content/docs/windows-host.md | 4 ++- web/messages/de.json | 2 ++ web/messages/en.json | 2 ++ web/src/sections/Host/GpuCard.tsx | 44 +++++++++++++++++++++++++ 8 files changed, 97 insertions(+), 3 deletions(-) diff --git a/api/openapi.json b/api/openapi.json index 5f884a8f..cc07406e 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -10,7 +10,7 @@ "name": "MIT OR Apache-2.0", "identifier": "MIT OR Apache-2.0" }, - "version": "0.18.0" + "version": "0.19.2" }, "paths": { "/api/v1/clients": { @@ -4506,6 +4506,13 @@ } ] }, + "encoder_pin": { + "type": [ + "string", + "null" + ], + "description": "`PUNKTFUNK_ENCODER` (the host.env encoder pin), when set to something other than `auto`\n(e.g. `qsv`, `nvenc`, `amf`, `software`). A pin whose vendor contradicts the selected\nGPU is overridden at session open — the adapter wins — so the console can warn that the\npin is stale rather than letting the selection look broken." + }, "env_override": { "type": [ "string", diff --git a/crates/punktfunk-host/src/mgmt/gpu.rs b/crates/punktfunk-host/src/mgmt/gpu.rs index fdeff98e..6f2a237b 100644 --- a/crates/punktfunk-host/src/mgmt/gpu.rs +++ b/crates/punktfunk-host/src/mgmt/gpu.rs @@ -66,6 +66,11 @@ pub(crate) struct GpuState { /// `PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is /// `auto`; a manual preference overrides it. env_override: Option, + /// `PUNKTFUNK_ENCODER` (the host.env encoder pin), when set to something other than `auto` + /// (e.g. `qsv`, `nvenc`, `amf`, `software`). A pin whose vendor contradicts the selected + /// GPU is overridden at session open — the adapter wins — so the console can warn that the + /// pin is stale rather than letting the selection look broken. + encoder_pin: Option, /// The GPU the next session will use. selected: Option, /// The GPU live sessions use right now (absent while nothing is streaming). @@ -143,11 +148,36 @@ pub(crate) fn gpu_state() -> GpuState { .render_adapter .clone() .filter(|s| !s.is_empty()), + encoder_pin: encoder_pin_of(&pf_host_config::config().encoder_pref), selected, active, } } +/// The `PUNKTFUNK_ENCODER` value worth surfacing to the console: `None` for unset/empty and for +/// an explicit `auto` (both mean "derive from the selected adapter" — nothing is pinned). +fn encoder_pin_of(pref: &str) -> Option { + match pref { + "" | "auto" => None, + pin => Some(pin.to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Only a real pin surfaces to the console — the `auto` spellings pin nothing, and showing + /// them would put a permanent scary note under every default install's GPU card. + #[test] + fn encoder_pin_surfaces_only_real_pins() { + assert_eq!(encoder_pin_of(""), None); + assert_eq!(encoder_pin_of("auto"), None); + assert_eq!(encoder_pin_of("qsv"), Some("qsv".into())); + assert_eq!(encoder_pin_of("software"), Some("software".into())); + } +} + /// GPU inventory and selection /// /// Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session diff --git a/crates/punktfunk-host/src/mgmt/tests.rs b/crates/punktfunk-host/src/mgmt/tests.rs index 22575c64..1100c6b8 100644 --- a/crates/punktfunk-host/src/mgmt/tests.rs +++ b/crates/punktfunk-host/src/mgmt/tests.rs @@ -1234,6 +1234,13 @@ async fn gpu_endpoints_list_and_validate() { assert_eq!(s, StatusCode::OK); assert!(b["gpus"].is_array()); assert!(b["mode"].is_string()); + // The host.env encoder pin is part of the schema (null when nothing is pinned) — the + // console warns off it when a pin contradicts the selected GPU (the pin is overridden at + // session open, and without this field the selection would just look broken). + assert!( + b.as_object().unwrap().contains_key("encoder_pin"), + "listGpus must carry encoder_pin" + ); // Unknown mode → 400. let (s, _) = send( diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index 2f5118ee..47916e23 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -38,7 +38,7 @@ redundant or stale. | `PUNKTFUNK_VIDEO_SOURCE` | `virtual` · `portal` | `virtual` creates a per-client display at the client's exact mode (the normal choice). `portal` captures an existing monitor instead. | | `PUNKTFUNK_ZEROCOPY` | `1` · `0` *(default on)* | GPU zero-copy capture→encode (dmabuf → CUDA → NVENC, or D3D11 on Windows). **On by default** — no need to set it; it falls back to a CPU path automatically. Set `0` to force the CPU path. One exception: Windows **Intel/QSV** keeps the CPU path by default until zero-copy is validated on Intel hardware — set `1` to try it there. | | `PUNKTFUNK_INPUT_BACKEND` | `libei` · `gamescope` · `wlr` · `uinput` | How input is injected. `libei` for GNOME/KDE, `gamescope` for Bazzite/gamescope, `wlr` for Sway/wlroots **and Hyprland**. Auto-detected with the compositor. | -| `PUNKTFUNK_ENCODER` | `auto` · `nvenc` · `vaapi` (Linux) · `amf` · `qsv` (Windows) · `software` | Encoder backend. `auto` (default) detects the GPU vendor: NVIDIA→NVENC, AMD→VAAPI/AMF, Intel→VAAPI/QSV. `software` (aliases `sw`/`openh264`) is the GPU-less H.264 path on both platforms — on Windows `auto` falls back to it when no GPU is found; on Linux it is **explicit-only** (`auto` never picks it). | +| `PUNKTFUNK_ENCODER` | `auto` · `nvenc` · `vaapi` (Linux) · `amf` · `qsv` (Windows) · `software` | Encoder backend. `auto` (default) detects the GPU vendor: NVIDIA→NVENC, AMD→VAAPI/AMF, Intel→VAAPI/QSV. `software` (aliases `sw`/`openh264`) is the GPU-less H.264 path on both platforms — on Windows `auto` falls back to it when no GPU is found; on Linux it is **explicit-only** (`auto` never picks it). On a multi-GPU Windows box a forced hardware backend whose vendor contradicts the selected GPU (web-console preference) is **overridden** — the adapter wins and the host logs a warning; remove the stale pin. | | `PUNKTFUNK_RENDER_NODE` | path | Linux DRM render node for zero-copy (default `/dev/dri/renderD128`). Set on multi-GPU boxes to pick the right GPU. | Resolution and refresh are **not** set here — **the client chooses them.** When a device connects, diff --git a/docs-site/content/docs/windows-host.md b/docs-site/content/docs/windows-host.md index 4ad9fd74..bb40e861 100644 --- a/docs-site/content/docs/windows-host.md +++ b/docs-site/content/docs/windows-host.md @@ -85,7 +85,9 @@ and enter the PIN on your [client](/docs/clients). The host's own management API The service reads `%ProgramData%\punktfunk\host.env`. The defaults work out of the box; common knobs: - `PUNKTFUNK_ENCODER=auto` — `auto` picks NVENC/AMF/QSV by GPU vendor. Force one with `nvenc`, `amf`, - `qsv`, or `sw` (software). + `qsv`, or `sw` (software). On a multi-GPU box the [web console's GPU preference](/docs/web-console) + wins: a forced backend whose vendor doesn't match the selected GPU is ignored (the host logs a + warning) — remove the stale line rather than fighting it. - `PUNKTFUNK_HOST_CMD` — the service runs `serve --gamestream` by default (native punktfunk/1 **plus** the GameStream/Moonlight-compat planes). Set it to `serve` for a **secure native-only** host with no GameStream surface (GameStream pairs over plain HTTP and uses weaker legacy encryption — trusted LAN diff --git a/web/messages/de.json b/web/messages/de.json index 975b24af..248b092b 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -56,6 +56,8 @@ "gpu_none": "Keine GPUs erkannt.", "gpu_missing_warning": "Die bevorzugte GPU „{name}“ ist nicht vorhanden — stattdessen wird automatisch gewählt.", "gpu_env_note": "PUNKTFUNK_RENDER_ADAPTER={value} bindet die GPU im Automatikmodus.", + "gpu_encoder_pin_note": "PUNKTFUNK_ENCODER={value} bindet das Encoder-Backend.", + "gpu_encoder_pin_warning": "PUNKTFUNK_ENCODER={value} bindet einen {vendor}-Encoder, aber die GPU der nächsten Sitzung ist „{name}“ — die veraltete Bindung sollte aus host.env entfernt werden.", "host_displays": "Virtuelle Displays", "host_displays_help": "Wie virtuelle Displays erstellt, aktiv gehalten und angeordnet werden. Wähle eine Voreinstellung oder „Benutzerdefiniert“, um Optionen direkt zu setzen. Eine Änderung gilt ab der nächsten Sitzung.", "display_config_title": "Konfiguration", diff --git a/web/messages/en.json b/web/messages/en.json index 4a07ddba..18629e52 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -56,6 +56,8 @@ "gpu_none": "No GPUs detected.", "gpu_missing_warning": "The preferred GPU “{name}” is not present — automatic selection is used instead.", "gpu_env_note": "PUNKTFUNK_RENDER_ADAPTER={value} pins the GPU while in automatic mode.", + "gpu_encoder_pin_note": "PUNKTFUNK_ENCODER={value} pins the encoder backend.", + "gpu_encoder_pin_warning": "PUNKTFUNK_ENCODER={value} pins a {vendor} encoder, but the next session's GPU is “{name}” — remove the stale pin from host.env.", "host_displays": "Virtual displays", "host_displays_help": "How virtual displays are created, kept alive, and arranged. Pick a preset, or choose Custom to set options directly. A change applies to the next session.", "display_config_title": "Configuration", diff --git a/web/src/sections/Host/GpuCard.tsx b/web/src/sections/Host/GpuCard.tsx index 0bec8d75..fffe7ad3 100644 --- a/web/src/sections/Host/GpuCard.tsx +++ b/web/src/sections/Host/GpuCard.tsx @@ -38,6 +38,49 @@ export const GpuSection: FC = () => { const fmtVram = (mb: number) => mb >= 1024 ? `${Math.round(mb / 1024)} GiB` : `${mb} MiB`; +/** + * The vendor an explicit `PUNKTFUNK_ENCODER` pin can open on (display name) — the console mirror + * of the host's backend→vendor table. Vendor-agnostic pins (software) and unknown/multi-vendor + * spellings (vaapi, vulkan, pyrowave) map to nothing: no conflict to warn about. + */ +const encoderPinVendor: Record = { + nvenc: "NVIDIA", + nvidia: "NVIDIA", + cuda: "NVIDIA", + hw: "NVIDIA", + amf: "AMD", + amd: "AMD", + qsv: "Intel", + intel: "Intel", +}; + +/** + * The host.env encoder pin, surfaced so a conflicting GPU choice doesn't just look broken: amber + * when the pin's vendor contradicts the next session's GPU (the host overrides the pin at session + * open — the stale pin should be removed), a muted note otherwise. + */ +const EncoderPinNote: FC<{ state: GpuState; pin: string }> = ({ + state, + pin, +}) => { + const vendor = encoderPinVendor[pin]; + const conflicting = + vendor && state.selected && state.selected.vendor !== vendor.toLowerCase(); + return conflicting && state.selected ? ( +

+ {m.gpu_encoder_pin_warning({ + value: pin, + vendor, + name: state.selected.name, + })} +

+ ) : ( +

+ {m.gpu_encoder_pin_note({ value: pin })} +

+ ); +}; + /** * GPU list in the compositors-card style: per-GPU badges for the manual pick ("Preferred"), what * the next session will use ("Next session"), and what live sessions encode on right now @@ -139,6 +182,7 @@ export const GpuCard: FC<{ {m.gpu_env_note({ value: s.env_override })}

)} + {s?.encoder_pin && }