feat(mgmt/web): surface the host.env encoder pin so a conflicting GPU choice isn't invisible
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 <noreply@anthropic.com>
This commit is contained in:
+8
-1
@@ -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",
|
||||
|
||||
@@ -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<String>,
|
||||
/// `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<String>,
|
||||
/// The GPU the next session will use.
|
||||
selected: Option<ApiSelectedGpu>,
|
||||
/// 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<String> {
|
||||
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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 ? (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-500">
|
||||
{m.gpu_encoder_pin_warning({
|
||||
value: pin,
|
||||
vendor,
|
||||
name: state.selected.name,
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{m.gpu_encoder_pin_note({ value: pin })}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 })}
|
||||
</p>
|
||||
)}
|
||||
{s?.encoder_pin && <EncoderPinNote state={s} pin={s.encoder_pin} />}
|
||||
</QueryState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user