diff --git a/crates/punktfunk-host/src/config.rs b/crates/punktfunk-host/src/config.rs index b0b7e35..90e8a90 100644 --- a/crates/punktfunk-host/src/config.rs +++ b/crates/punktfunk-host/src/config.rs @@ -25,9 +25,12 @@ //! - **Path / genuinely-dynamic reads**: the config-dir resolution, `PATH` executable search, the //! env-forward-to-child loop, `PUNKTFUNK_MGMT_TOKEN`, `PUNKTFUNK_HOST_CMD`, `PUNKTFUNK_RENDER_NODE`. //! -//! `PUNKTFUNK_ZEROCOPY` note: this field uses **presence** semantics (`var_os(..).is_some()`) to match the -//! Windows `encode/ffmpeg_win.rs` reader. The Linux `zerocopy` module keeps its own *truthy* parser -//! (`1|true|yes|on`) — the two are independent features that share a name; do NOT conflate them. +//! `PUNKTFUNK_ZEROCOPY` note: this field is a **tri-state override** (`None` = unset). Unset defers to +//! the per-vendor default in `encode/ffmpeg_win.rs::zerocopy_enabled` (AMF on — on-glass validated +//! 2026-07-06; QSV off until validated on Intel glass); an explicit value forces it (`0|false|off|no` +//! = off, anything else = on, so the old presence-style `=1` keeps working). The Linux `zerocopy` +//! module keeps its own *truthy* parser (`1|true|yes|on`) — the two are independent features that +//! share a name; do NOT conflate them. use std::sync::OnceLock; @@ -43,8 +46,9 @@ pub struct HostConfig { pub render_adapter: Option, /// `PUNKTFUNK_IDD_DEPTH` — IDD-push pipeline depth override (default 2; the call site clamps to its `OUT_RING`). pub idd_depth: usize, - /// `PUNKTFUNK_ZEROCOPY` — opt into the Windows D3D11 zero-copy encode path (presence semantics; see module docs). - pub zerocopy: bool, + /// `PUNKTFUNK_ZEROCOPY` — Windows D3D11 zero-copy encode input override. `None` (unset) defers to + /// the per-vendor default (AMF on, QSV off — see module docs and `encode/ffmpeg_win.rs`). + pub zerocopy: Option, /// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit). pub ten_bit: bool, /// `PUNKTFUNK_444` — host policy gate for full-chroma HEVC 4:4:4 (Range Extensions). Honored only @@ -84,7 +88,9 @@ impl HostConfig { idd_depth: val("PUNKTFUNK_IDD_DEPTH") .and_then(|s| s.parse::().ok()) .unwrap_or(2), - zerocopy: flag("PUNKTFUNK_ZEROCOPY"), + zerocopy: val("PUNKTFUNK_ZEROCOPY").map(|s| { + !matches!(s.trim().to_ascii_lowercase().as_str(), "0" | "false" | "off" | "no") + }), ten_bit: flag("PUNKTFUNK_10BIT"), four_four_four: flag("PUNKTFUNK_444"), perf: flag("PUNKTFUNK_PERF"), diff --git a/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs b/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs index 09c26fd..7228b1f 100644 --- a/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs +++ b/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs @@ -8,21 +8,23 @@ //! BGRA/Rgb10a2 as a fallback) on the capturer's own `ID3D11Device`. Two input paths, chosen lazily //! from the first frame and the `PUNKTFUNK_ZEROCOPY` knob: //! -//! * **System-memory** ([`SystemInner`], the default): read the captured D3D11 surface back to a CPU +//! * **System-memory** ([`SystemInner`]): read the captured D3D11 surface back to a CPU //! NV12/P010 [`AVFrame`] (a same-format `CopyResource` → staging → `Map`, plus a `swscale` step for //! the BGRA fallback) and `avcodec_send_frame` it. AMF/QSV upload it internally. One -//! GPU→CPU→GPU round-trip per frame — the robust path, and the only one that can be brought up -//! without on-glass validation (it is the analogue of the VAAPI "CPU input" fallback). -//! * **Zero-copy D3D11** ([`ZeroCopyInner`], `PUNKTFUNK_ZEROCOPY=1`): wrap the capturer's -//! `ID3D11Device` as an `AV_HWDEVICE_TYPE_D3D11VA` hwdevice (shared, *not* a second device — the -//! capture textures are not shared-handle, so a different device couldn't read them), keep an -//! FFmpeg D3D11 frames pool, `CopySubresourceRegion` the captured texture into a pooled array -//! slice (a GPU-local copy, like NVENC's CUDA path), then feed AMF `AV_PIX_FMT_D3D11` directly, -//! or map the D3D11 frame to a derived QSV surface for QSV. If the hw setup fails to open, this -//! falls back to the system-memory path for the session. +//! GPU→CPU→GPU round-trip per frame — the robust path, the QSV default, and the automatic +//! fallback when the zero-copy setup fails (it is the analogue of the VAAPI "CPU input" fallback). +//! * **Zero-copy D3D11** ([`ZeroCopyInner`], the AMF default; see [`zerocopy_enabled`]): wrap the +//! capturer's `ID3D11Device` as an `AV_HWDEVICE_TYPE_D3D11VA` hwdevice (shared, *not* a second +//! device — the capture textures are not shared-handle, so a different device couldn't read them), +//! keep an FFmpeg D3D11 frames pool, `CopySubresourceRegion` the captured texture into a pooled +//! array slice (a GPU-local copy, like NVENC's CUDA path), then feed AMF `AV_PIX_FMT_D3D11` +//! directly, or map the D3D11 frame to a derived QSV surface for QSV. If the hw setup fails to +//! open, this falls back to the system-memory path for the session. //! -//! **Status: compiles in CI; not yet on-glass validated** (no AMD/Intel Windows box in the lab as of -//! 2026-06-22). The system path is the conservative default; zero-copy is opt-in until validated. +//! **Status:** AMF on-glass validated 2026-07-06 (Ryzen 7000 iGPU, 1080p120 HDR P010, both input +//! paths; zero-copy cut `submit_us` p50 2.8 ms → 0.26 ms) — zero-copy is the AMF default. QSV is +//! still not on-glass validated (no Intel Windows box in the lab), so its zero-copy path stays +//! opt-in via `PUNKTFUNK_ZEROCOPY=1`. //! //! Raw FFI: `ffmpeg-next` has no hwcontext wrappers for D3D11VA, so the hwdevice/hwframes calls go //! through `ffmpeg::ffi` (= `ffmpeg_sys_next`), exactly as the Linux CUDA/VAAPI paths do. The @@ -108,10 +110,16 @@ impl WinVendor { } } -/// Is the zero-copy D3D11 path enabled? Opt-in (`PUNKTFUNK_ZEROCOPY=1`) until on-glass validated; -/// the default is the robust system-memory readback path. -fn zerocopy_enabled() -> bool { - crate::config::config().zerocopy +/// Is the zero-copy D3D11 path enabled for this vendor? An explicit `PUNKTFUNK_ZEROCOPY` +/// (`0|false|off|no` = off, anything else = on) overrides; unset defers to the per-vendor default: +/// **on for AMF** — on-glass validated 2026-07-06 (Ryzen iGPU, 1080p120 HDR P010: `submit_us` p50 +/// 2.8 ms → 0.26 ms vs readback) — and **off for QSV** until validated on Intel glass (the +/// open-failure fallback only catches *setup* errors; a derive that opens but maps wrong would +/// corrupt silently, so it stays opt-in per the probe-never-assume rule). +fn zerocopy_enabled(vendor: WinVendor) -> bool { + crate::config::config() + .zerocopy + .unwrap_or(matches!(vendor, WinVendor::Amf)) } /// The swscale *source* pixel format for a captured packed-RGB/BGR layout (8-bit BGRA fallback only). @@ -771,9 +779,9 @@ impl Drop for SystemInner { } // --------------------------------------------------------------------------------------------- -// Zero-copy D3D11 path (PUNKTFUNK_ZEROCOPY=1): share the capture device, pool D3D11 frames, copy -// the captured texture into a pooled slice, feed AMF directly / map to QSV. Falls back to the -// system path if the hw setup fails to open. Untested on glass — opt-in only for now. +// Zero-copy D3D11 path (the AMF default; QSV opt-in — see `zerocopy_enabled`): share the capture +// device, pool D3D11 frames, copy the captured texture into a pooled slice, feed AMF directly / +// map to QSV. Falls back to the system path if the hw setup fails to open. // --------------------------------------------------------------------------------------------- struct D3d11Hw { @@ -1199,7 +1207,7 @@ impl FfmpegWinEncoder { } self.inner = None; self.bound_device = dev_raw; - let inner = if zerocopy_enabled() { + let inner = if zerocopy_enabled(self.vendor) { match ZeroCopyInner::open( self.vendor, self.codec, diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index 878beff..55db3d7 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -36,7 +36,7 @@ On Linux the host **rewrites `WAYLAND_DISPLAY` / `XDG_CURRENT_DESKTOP` / `XDG_RU |---|---|---| | `PUNKTFUNK_COMPOSITOR` | `kwin` · `mutter` · `gamescope` · `wlroots` (aliases: `kde`/`plasma`, `gnome`, `sway`/`hyprland`) | Which backend creates the virtual display. **Leave unset to auto-detect;** set only to force one. | | `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. | +| `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. 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_RENDER_NODE` | path | Linux DRM render node for zero-copy (default `/dev/dri/renderD128`). Set on multi-GPU boxes to pick the right GPU. |