feat(host): default the Windows AMF encode input to zero-copy D3D11
On-glass A/B on the Ryzen 7000 iGPU (1080p120 HDR P010, hevc_amf,
PUNKTFUNK_PERF stage split): the system-memory readback costs the encode
thread 2.7-2.9 ms p50 (6.6 ms p99) per frame in submit; the zero-copy D3D11
pool path does the same work in 0.26 ms p50 (0.5 ms p99) — and on an iGPU the
readback also burns the shared memory bandwidth the game needs. The docs-site
already promised "on by default ... D3D11 on Windows" since the Linux flip
(2f9e61d was Linux-only); the Windows code now delivers it.
PUNKTFUNK_ZEROCOPY becomes a tri-state override: unset defers to a per-vendor
default in zerocopy_enabled(vendor) — ON for AMF (validated above; open
failures still fall back to system-memory readback), OFF for QSV until it is
validated on Intel glass (the fallback only catches *setup* errors; a QSV
derive that opens but maps wrong would corrupt silently, so probe-never-assume
applies). Explicit values force either way: 0|false|off|no = readback,
anything else = zero-copy, so the old presence-style =1 keeps working.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,12 @@
|
|||||||
//! - **Path / genuinely-dynamic reads**: the config-dir resolution, `PATH` executable search, the
|
//! - **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`.
|
//! 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
|
//! `PUNKTFUNK_ZEROCOPY` note: this field is a **tri-state override** (`None` = unset). Unset defers to
|
||||||
//! Windows `encode/ffmpeg_win.rs` reader. The Linux `zerocopy` module keeps its own *truthy* parser
|
//! the per-vendor default in `encode/ffmpeg_win.rs::zerocopy_enabled` (AMF on — on-glass validated
|
||||||
//! (`1|true|yes|on`) — the two are independent features that share a name; do NOT conflate them.
|
//! 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;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
@@ -43,8 +46,9 @@ pub struct HostConfig {
|
|||||||
pub render_adapter: Option<String>,
|
pub render_adapter: Option<String>,
|
||||||
/// `PUNKTFUNK_IDD_DEPTH` — IDD-push pipeline depth override (default 2; the call site clamps to its `OUT_RING`).
|
/// `PUNKTFUNK_IDD_DEPTH` — IDD-push pipeline depth override (default 2; the call site clamps to its `OUT_RING`).
|
||||||
pub idd_depth: usize,
|
pub idd_depth: usize,
|
||||||
/// `PUNKTFUNK_ZEROCOPY` — opt into the Windows D3D11 zero-copy encode path (presence semantics; see module docs).
|
/// `PUNKTFUNK_ZEROCOPY` — Windows D3D11 zero-copy encode input override. `None` (unset) defers to
|
||||||
pub zerocopy: bool,
|
/// the per-vendor default (AMF on, QSV off — see module docs and `encode/ffmpeg_win.rs`).
|
||||||
|
pub zerocopy: Option<bool>,
|
||||||
/// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit).
|
/// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit).
|
||||||
pub ten_bit: bool,
|
pub ten_bit: bool,
|
||||||
/// `PUNKTFUNK_444` — host policy gate for full-chroma HEVC 4:4:4 (Range Extensions). Honored only
|
/// `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")
|
idd_depth: val("PUNKTFUNK_IDD_DEPTH")
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
.unwrap_or(2),
|
.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"),
|
ten_bit: flag("PUNKTFUNK_10BIT"),
|
||||||
four_four_four: flag("PUNKTFUNK_444"),
|
four_four_four: flag("PUNKTFUNK_444"),
|
||||||
perf: flag("PUNKTFUNK_PERF"),
|
perf: flag("PUNKTFUNK_PERF"),
|
||||||
|
|||||||
@@ -8,21 +8,23 @@
|
|||||||
//! BGRA/Rgb10a2 as a fallback) on the capturer's own `ID3D11Device`. Two input paths, chosen lazily
|
//! 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:
|
//! 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
|
//! 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
|
//! 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
|
//! GPU→CPU→GPU round-trip per frame — the robust path, the QSV default, and the automatic
|
||||||
//! without on-glass validation (it is the analogue of the VAAPI "CPU input" fallback).
|
//! fallback when the zero-copy setup fails (it is the analogue of the VAAPI "CPU input" fallback).
|
||||||
//! * **Zero-copy D3D11** ([`ZeroCopyInner`], `PUNKTFUNK_ZEROCOPY=1`): wrap the capturer's
|
//! * **Zero-copy D3D11** ([`ZeroCopyInner`], the AMF default; see [`zerocopy_enabled`]): wrap the
|
||||||
//! `ID3D11Device` as an `AV_HWDEVICE_TYPE_D3D11VA` hwdevice (shared, *not* a second device — the
|
//! capturer's `ID3D11Device` as an `AV_HWDEVICE_TYPE_D3D11VA` hwdevice (shared, *not* a second
|
||||||
//! capture textures are not shared-handle, so a different device couldn't read them), keep an
|
//! device — the capture textures are not shared-handle, so a different device couldn't read them),
|
||||||
//! FFmpeg D3D11 frames pool, `CopySubresourceRegion` the captured texture into a pooled array
|
//! keep an FFmpeg D3D11 frames pool, `CopySubresourceRegion` the captured texture into a pooled
|
||||||
//! slice (a GPU-local copy, like NVENC's CUDA path), then feed AMF `AV_PIX_FMT_D3D11` directly,
|
//! array slice (a GPU-local copy, like NVENC's CUDA path), then feed AMF `AV_PIX_FMT_D3D11`
|
||||||
//! or map the D3D11 frame to a derived QSV surface for QSV. If the hw setup fails to open, this
|
//! directly, or map the D3D11 frame to a derived QSV surface for QSV. If the hw setup fails to
|
||||||
//! falls back to the system-memory path for the session.
|
//! 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
|
//! **Status:** AMF on-glass validated 2026-07-06 (Ryzen 7000 iGPU, 1080p120 HDR P010, both input
|
||||||
//! 2026-06-22). The system path is the conservative default; zero-copy is opt-in until validated.
|
//! 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
|
//! 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
|
//! 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;
|
/// Is the zero-copy D3D11 path enabled for this vendor? An explicit `PUNKTFUNK_ZEROCOPY`
|
||||||
/// the default is the robust system-memory readback path.
|
/// (`0|false|off|no` = off, anything else = on) overrides; unset defers to the per-vendor default:
|
||||||
fn zerocopy_enabled() -> bool {
|
/// **on for AMF** — on-glass validated 2026-07-06 (Ryzen iGPU, 1080p120 HDR P010: `submit_us` p50
|
||||||
crate::config::config().zerocopy
|
/// 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).
|
/// 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
|
// Zero-copy D3D11 path (the AMF default; QSV opt-in — see `zerocopy_enabled`): share the capture
|
||||||
// the captured texture into a pooled slice, feed AMF directly / map to QSV. Falls back to the
|
// device, pool D3D11 frames, copy the captured texture into a pooled slice, feed AMF directly /
|
||||||
// system path if the hw setup fails to open. Untested on glass — opt-in only for now.
|
// map to QSV. Falls back to the system path if the hw setup fails to open.
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
struct D3d11Hw {
|
struct D3d11Hw {
|
||||||
@@ -1199,7 +1207,7 @@ impl FfmpegWinEncoder {
|
|||||||
}
|
}
|
||||||
self.inner = None;
|
self.inner = None;
|
||||||
self.bound_device = dev_raw;
|
self.bound_device = dev_raw;
|
||||||
let inner = if zerocopy_enabled() {
|
let inner = if zerocopy_enabled(self.vendor) {
|
||||||
match ZeroCopyInner::open(
|
match ZeroCopyInner::open(
|
||||||
self.vendor,
|
self.vendor,
|
||||||
self.codec,
|
self.codec,
|
||||||
|
|||||||
@@ -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_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_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_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_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. |
|
| `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. |
|
||||||
|
|||||||
Reference in New Issue
Block a user