feat(windows): AMD (AMF) + Intel (QSV) hardware encode on the Windows host

The Windows host was NVIDIA-only (NVENC) with an openh264 software fallback. Add
AMD AMF and Intel QSV via libavcodec — the Windows analogue of the Linux VAAPI
backend — so one installer serves all three GPU vendors.

- encode/ffmpeg_win.rs: new WinVendor{Amf,Qsv} encoder. System-memory NV12/P010
  readback (default, robust) + opt-in zero-copy D3D11 (PUNKTFUNK_ZEROCOPY: shares
  the capturer's ID3D11Device; AMF takes AV_PIX_FMT_D3D11, QSV derives a QSV frames
  ctx and maps) with a system fallback for the format-group mismatch the capturer's
  video-processor fallback can produce. HDR Main10 (P010 + BT.2020/PQ VUI; an
  Rgb10a2->P010 swscale covers the shader fallback).
- encode.rs: Codec::amf_name/qsv_name; open_video + windows_resolved_backend()
  resolve PUNKTFUNK_ENCODER=auto|nvenc|amf|qsv|sw via a DXGI adapter VendorId probe.
- capture/dxgi.rs: gpu_mode mirrors the resolved backend (D3D11 NV12/P010 for AMF/QSV).
- gamestream/serverinfo.rs: GPU-aware codec advertisement (windows_codec_support;
  AV1 gated to RDNA3+/Arc, like the VAAPI path).
- Cargo.toml: amf-qsv feature (optional ffmpeg-next in the windows target block).
- CI/installer: windows-host.yml sets FFMPEG_DIR + builds --features nvenc,amf-qsv;
  the Inno installer bundles the FFmpeg DLLs; host.env default nvenc -> auto.

CI-green target; AMF/QSV not yet on-glass validated (no AMD/Intel Windows box in the
lab) — NVENC stays live-validated. An adversarial-review pass caught + fixed real
FFI bugs (AV_PIX_FMT_P010 is a macro -> P010LE; windows-rs 0.62 GetImmediateContext/
GetDesc1 return Result; AV_HWFRAME_MAP_* is a bindgen enum with no BitOr).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 10:31:54 +00:00
parent fde438a1ed
commit 72eeedc4da
12 changed files with 1515 additions and 86 deletions
+17 -6
View File
@@ -20,9 +20,12 @@
# an ephemeral self-signed cert is generated and its public .cer published next to the installer # an ephemeral self-signed cert is generated and its public .cer published next to the installer
# (import once to LocalMachine\TrustedPublisher). See packaging/windows/pack-host-installer.ps1. # (import once to LocalMachine\TrustedPublisher). See packaging/windows/pack-host-installer.ps1.
# #
# NVENC: the host builds with --features nvenc; the only link need is nvencodeapi.lib, synthesised # GPU backends: the host builds with --features nvenc,amf-qsv = all three vendors in one installer.
# from a 2-export .def with llvm-dlltool (no GPU/SDK at build time). The resulting exe is NVIDIA-only # - NVENC (NVIDIA, direct SDK): the only link need is nvencodeapi.lib, synthesised from a 2-export
# by design — CI never launches it, so no GPU is needed here. # .def with llvm-dlltool (no GPU/SDK at build time).
# - AMF/QSV (AMD/Intel, libavcodec): link-imports the FFmpeg libs from FFMPEG_DIR (the BtbN gpl-shared
# tree the client uses; includes the *_amf/*_qsv encoders) and bundles its DLLs into the installer.
# CI never launches the exe, so no GPU is needed here — this is build + Windows clippy coverage only.
name: windows-host name: windows-host
on: on:
@@ -59,6 +62,13 @@ jobs:
# (pwsh Out-File utf8 = no BOM, unlike Windows PowerShell 5.1 — keeps the first line clean). # (pwsh Out-File utf8 = no BOM, unlike Windows PowerShell 5.1 — keeps the first line clean).
"CARGO_TARGET_DIR=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "CARGO_TARGET_DIR=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"CARGO_WORKSPACE_DIR=$env:GITHUB_WORKSPACE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "CARGO_WORKSPACE_DIR=$env:GITHUB_WORKSPACE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
# FFMPEG_DIR: the same BtbN gpl-shared x64 tree the Windows CLIENT links against (provisioned
# by scripts/ci/setup-windows-runner.ps1). The host's AMD/Intel AMF/QSV encode backend
# (--features amf-qsv) link-imports avcodec/avutil/swscale from it; pack-host-installer.ps1
# then bundles its bin\*.dll into the installer. LIBCLANG_PATH is in the runner daemon env.
if (-not $env:FFMPEG_DIR) {
"FFMPEG_DIR=C:\Users\Public\ffmpeg" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
}
$v = if ($env:GITHUB_REF -like 'refs/tags/v*') { $v = if ($env:GITHUB_REF -like 'refs/tags/v*') {
$env:GITHUB_REF_NAME -replace '^v', '' $env:GITHUB_REF_NAME -replace '^v', ''
} else { } else {
@@ -74,14 +84,15 @@ jobs:
& packaging/windows/nvenc/gen-nvenc-importlib.ps1 -OutDir C:\t\nvenc & packaging/windows/nvenc/gen-nvenc-importlib.ps1 -OutDir C:\t\nvenc
"PUNKTFUNK_NVENC_LIB_DIR=C:\t\nvenc" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "PUNKTFUNK_NVENC_LIB_DIR=C:\t\nvenc" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build (release, nvenc) - name: Build (release, nvenc + amf-qsv)
shell: pwsh shell: pwsh
run: cargo build --release -p punktfunk-host --features nvenc # All-vendor host: NVENC (NVIDIA, direct SDK) + AMF/QSV (AMD/Intel, libavcodec via FFMPEG_DIR).
run: cargo build --release -p punktfunk-host --features nvenc,amf-qsv
- name: Clippy (host, Windows) - name: Clippy (host, Windows)
shell: pwsh shell: pwsh
# First-ever Windows lint coverage for the host (Linux CI never lints the windows-cfg code). # First-ever Windows lint coverage for the host (Linux CI never lints the windows-cfg code).
run: cargo clippy -p punktfunk-host --features nvenc -- -D warnings run: cargo clippy -p punktfunk-host --features nvenc,amf-qsv -- -D warnings
- name: Ensure Inno Setup - name: Ensure Inno Setup
shell: pwsh shell: pwsh
+21 -11
View File
@@ -74,17 +74,26 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
Clients auto-resolve the type from the physical controller (DS5→DualSense, DS4→DualShock 4, Clients auto-resolve the type from the physical controller (DS5→DualSense, DS4→DualShock 4,
Xbox One→Xbox One). Windows-host DualShock 4 (ViGEm) is not yet wired — Windows clients asking for Xbox One→Xbox One). Windows-host DualShock 4 (ViGEm) is not yet wired — Windows clients asking for
DS4 get Xbox 360 for now. DS4 get Xbox 360 for now.
- **Windows host: implemented and shipping (NVIDIA-only, x64-only).** `#[cfg(windows)]` backends - **Windows host: implemented and shipping (all-vendor, x64-only).** `#[cfg(windows)]` backends
behind the same traits as Linux — DXGI Desktop Duplication capture (`capture/dxgi.rs`), **SudoVDA** behind the same traits as Linux — DXGI Desktop Duplication capture (`capture/dxgi.rs`), **SudoVDA**
virtual display per session (`vdisplay/sudovda.rs`), NVENC encode (`--features nvenc`), SendInput + virtual display per session (`vdisplay/sudovda.rs`), GPU encode (NVENC `--features nvenc`; AMD/Intel
**ViGEm** gamepads (`inject/gamepad_windows.rs`), WASAPI loopback + virtual mic (`audio/wasapi_*`). `--features amf-qsv`), SendInput + **ViGEm** gamepads (`inject/gamepad_windows.rs`), WASAPI loopback
Ships as a **signed Inno Setup installer** that registers a `LocalSystem` SCM service launching into + virtual mic (`audio/wasapi_*`). Ships as a **signed Inno Setup installer** that registers a
the interactive session for secure-desktop (UAC/lock-screen) capture (`service.rs`), bundles the `LocalSystem` SCM service launching into the interactive session for secure-desktop (UAC/lock-screen)
SudoVDA driver, and is published by `windows-host.yml`. **HDR (10-bit)**: WGC captures the HDR capture (`service.rs`), bundles the SudoVDA driver + the FFmpeg DLLs, and is published by
desktop as FP16/Rgb10a2 (DDA FP16 for the secure desktop), NVENC forces HEVC Main10 + BT.2020 PQ, `windows-host.yml`. **Encoder is GPU-aware** (`encode.rs` `open_video` + `windows_resolved_backend`):
the client auto-detects PQ from the HEVC VUI — gated by `PUNKTFUNK_10BIT` + client `VIDEO_CAP_10BIT`; `PUNKTFUNK_ENCODER=auto` (the host.env default) detects the DXGI adapter vendor → **NVENC** (NVIDIA,
**Windows host only** (the Linux host stays 8-bit, blocked upstream). Newer/less battle-tested than direct SDK, `encode/nvenc.rs`), **AMF** (AMD) / **QSV** (Intel) via libavcodec
the Linux host; no AMD/Intel/software encode path. Packaging: `packaging/windows/`. (`encode/ffmpeg_win.rs`, the Windows analogue of the Linux VAAPI backend — `WinVendor{Amf,Qsv}`,
system-memory NV12/P010 readback default + opt-in zero-copy D3D11 behind `PUNKTFUNK_ZEROCOPY` with a
system fallback), or software H.264 (`encode/sw.rs`, GPU-less). GameStream codec advertisement is
probed per-GPU on AMF/QSV (`windows_codec_support``serverinfo`, AV1 gated). **HDR (10-bit)**: WGC
captures the HDR desktop as FP16/Rgb10a2 (DDA FP16 for the secure desktop), the encoder forces HEVC
Main10 + BT.2020 PQ (NVENC ABGR10/P010; AMF/QSV P010 + a swscale Rgb10a2→P010 fallback), the client
auto-detects PQ from the HEVC VUI — gated by `PUNKTFUNK_10BIT` + client `VIDEO_CAP_10BIT`; **Windows
host only** (the Linux host stays 8-bit, blocked upstream). **AMF/QSV is CI-green but not yet
on-glass validated** (no AMD/Intel Windows box in the lab); NVENC is live-validated. Newer/less
battle-tested than the Linux host. Packaging: `packaging/windows/`.
## What's left ## What's left
@@ -243,6 +252,7 @@ crates/punktfunk-host/
vdisplay/{kwin,gamescope,mutter,wlroots}.rs per-compositor client-sized virtual outputs vdisplay/{kwin,gamescope,mutter,wlroots}.rs per-compositor client-sized virtual outputs
zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan) zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan)
inject/{libei,wlr,gamepad,dualsense}.rs input backends (uinput xpad + UHID DualSense) inject/{libei,wlr,gamepad,dualsense}.rs input backends (uinput xpad + UHID DualSense)
encode/{nvenc,linux,vaapi,ffmpeg_win,sw}.rs per-GPU encoders (NVENC · Linux NVENC/CUDA · VAAPI · AMF/QSV · openh264)
capture.rs · encode.rs · audio.rs · spike.rs · punktfunk1.rs · mgmt.rs · native_pairing.rs capture.rs · encode.rs · audio.rs · spike.rs · punktfunk1.rs · mgmt.rs · native_pairing.rs
clients/probe/ punktfunk/1 reference/probe client (headless test/measurement tool) clients/probe/ punktfunk/1 reference/probe client (headless test/measurement tool)
clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3) clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3)
@@ -250,7 +260,7 @@ clients/windows/ native Windows client (WinUI 3 via windows-reactor · D3D11 ·
clients/apple/ native macOS/iOS/tvOS client (Swift · VideoToolbox · GameController) clients/apple/ native macOS/iOS/tvOS client (Swift · VideoToolbox · GameController)
clients/android/ native Android client (Kotlin app + native/ Rust JNI core over punktfunk-core) clients/android/ native Android client (Kotlin app + native/ Rust JNI core over punktfunk-core)
clients/decky/ Steam Deck Decky plugin clients/decky/ Steam Deck Decky plugin
crates/punktfunk-host/src/{capture/dxgi,vdisplay/sudovda,inject/gamepad_windows,audio/wasapi_*,service}.rs Windows host backends crates/punktfunk-host/src/{capture/dxgi,vdisplay/sudovda,encode/ffmpeg_win,inject/gamepad_windows,audio/wasapi_*,service}.rs Windows host backends
web/ TanStack web console over the mgmt API (status · devices · pairing) web/ TanStack web console over the mgmt API (status · devices · pairing)
packaging/ apt(deb) · RPM/COPR · Arch/sysext · Flatpak · Bazzite bootc · Windows host installer (per-dir READMEs) packaging/ apt(deb) · RPM/COPR · Arch/sysext · Flatpak · Bazzite bootc · Windows host installer (per-dir READMEs)
tools/{loss-harness,latency-probe}/ measurement (plan §10) tools/{loss-harness,latency-probe}/ measurement (plan §10)
+10
View File
@@ -184,6 +184,12 @@ vigem-client = { version = "0.1", features = ["unstable_xtarget_notification"] }
# the crate builds on docs.rs/CI. We enable it so the GPU-less VM/CI compiles; the DirectX NVENC path # the crate builds on docs.rs/CI. We enable it so the GPU-less VM/CI compiles; the DirectX NVENC path
# never calls CUDA at runtime, so the pinned CUDA bindings version is irrelevant. # never calls CUDA at runtime, so the pinned CUDA bindings version is irrelevant.
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true } nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
# AMD (AMF) + Intel (QSV) hardware encode on Windows via libavcodec — the analogue of the Linux
# VAAPI backend (`src/encode/ffmpeg_win.rs`). Optional + behind the `amf-qsv` feature because it
# link-imports the FFmpeg libs at build time (needs a `FFMPEG_DIR` with the AMF/QSV encoders — the
# same BtbN gpl-shared tree the Windows client uses) and pulls the shared `avcodec/avutil/...` DLLs
# at runtime. `ffmpeg-sys-next` auto-detects the FFmpeg version (7.x/avcodec-61 or 8.x/62).
ffmpeg-next = { version = "8", optional = true }
[features] [features]
# NVENC hardware encode (Windows). OFF by default: it pulls the NVENC SDK, and the host then needs # NVENC hardware encode (Windows). OFF by default: it pulls the NVENC SDK, and the host then needs
@@ -191,3 +197,7 @@ nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional =
# time — i.e. `nvencodeapi.lib` from the NVIDIA Video Codec SDK (or an import lib generated from # time — i.e. `nvencodeapi.lib` from the NVIDIA Video Codec SDK (or an import lib generated from
# nvEncodeAPI64.dll) on the linker path. Build the GPU host with `--features nvenc`. # nvEncodeAPI64.dll) on the linker path. Build the GPU host with `--features nvenc`.
nvenc = ["dep:nvidia-video-codec-sdk"] nvenc = ["dep:nvidia-video-codec-sdk"]
# AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: it needs a
# `FFMPEG_DIR` (BtbN gpl-shared, includes `*_amf`/`*_qsv`) at build time and bundles the FFmpeg
# DLLs at runtime. Build the all-vendor GPU host with `--features nvenc,amf-qsv`.
amf-qsv = ["dep:ffmpeg-next"]
+8 -3
View File
@@ -2157,9 +2157,14 @@ impl DuplCapturer {
.ok() .ok()
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
.unwrap_or((2000 / refresh_hz.max(1)).max(100)); .unwrap_or((2000 / refresh_hz.max(1)).max(100));
let gpu_mode = std::env::var("PUNKTFUNK_ENCODER") // Produce GPU-resident D3D11 frames (zero-copy NVENC, or the NV12/P010 the AMF/QSV
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "nvenc" | "hw" | "nvidia")) // backends read back / import) whenever the resolved encode backend is a GPU one — so the
.unwrap_or(false); // capturer's output format matches the encoder's input. Only the software (GPU-less) path
// takes CPU staging. Mirrors `encode::open_video`'s dispatch exactly.
let gpu_mode = !matches!(
crate::encode::windows_resolved_backend(),
crate::encode::WindowsBackend::Software
);
// Read the source display's HDR mastering metadata while we still hold `output` (it is // Read the source display's HDR mastering metadata while we still hold `output` (it is
// moved into the struct below). Only meaningful for an HDR (FP16) duplication. // moved into the struct below). Only meaningful for an HDR (FP16) duplication.
let is_hdr_init = dd.ModeDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT; let is_hdr_init = dd.ModeDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT;
+209 -38
View File
@@ -49,6 +49,26 @@ impl Codec {
Codec::Av1 => "av1_vaapi", Codec::Av1 => "av1_vaapi",
} }
} }
/// The FFmpeg AMD **AMF** encoder name (the Windows AMD backend). Selected by name (the codec id
/// would pick the software encoder). AV1 (`av1_amf`) is RDNA3+/RX 7000+ — probe, never assume.
pub fn amf_name(self) -> &'static str {
match self {
Codec::H264 => "h264_amf",
Codec::H265 => "hevc_amf",
Codec::Av1 => "av1_amf",
}
}
/// The FFmpeg Intel **QSV** encoder name (the Windows Intel backend). Selected by name. AV1
/// (`av1_qsv`) is Arc/Xe2+; HEVC Main10 is Gen9.5+ — probe, never assume.
pub fn qsv_name(self) -> &'static str {
match self {
Codec::H264 => "h264_qsv",
Codec::H265 => "hevc_qsv",
Codec::Av1 => "av1_qsv",
}
}
} }
/// A hardware encoder. One per session; runs on the encode thread. /// A hardware encoder. One per session; runs on the encode thread.
@@ -198,49 +218,83 @@ pub fn open_video(
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
let _ = cuda; // always false on Windows (no Cuda payload) let _ = cuda; // always false on Windows (no Cuda payload)
let _ = bit_depth; // used by the NVENC path below; the software H.264 path is 8-bit only // NVIDIA → NVENC (direct SDK), AMD → AMF, Intel → QSV (both libavcodec), else → software
let pref = std::env::var("PUNKTFUNK_ENCODER") // H.264. `auto` (the default) resolves from the DXGI adapter vendor.
.unwrap_or_default() match windows_resolved_backend() {
.to_ascii_lowercase(); WindowsBackend::Nvenc => {
if matches!(pref.as_str(), "nvenc" | "hw" | "nvidia") { // Hardware path: NVENC over D3D11. The DXGI capturer switches to its zero-copy
// Hardware path: NVENC over D3D11. The DXGI capturer switches to its zero-copy // FramePayload::D3d11 output under the same env var so capture + encode share textures.
// FramePayload::D3d11 output under the same env var so capture + encode share textures. #[cfg(feature = "nvenc")]
#[cfg(feature = "nvenc")] {
{ nvenc::NvencD3d11Encoder::open(
let enc = nvenc::NvencD3d11Encoder::open( codec,
codec, format,
width,
height,
fps,
bitrate_bps,
bit_depth,
)
.map(|e| Box::new(e) as Box<dyn Encoder>)
}
#[cfg(not(feature = "nvenc"))]
{
anyhow::bail!(
"NVENC requested/detected but this host was built without it — rebuild \
with `--features nvenc` (needs the NVENC SDK's nvencodeapi.lib at link time)"
)
}
}
backend @ (WindowsBackend::Amf | WindowsBackend::Qsv) => {
// AMD AMF / Intel QSV via libavcodec (the Windows analogue of the Linux VAAPI path).
#[cfg(feature = "amf-qsv")]
{
let vendor = if matches!(backend, WindowsBackend::Amf) {
ffmpeg_win::WinVendor::Amf
} else {
ffmpeg_win::WinVendor::Qsv
};
ffmpeg_win::FfmpegWinEncoder::open(
vendor,
codec,
format,
width,
height,
fps,
bitrate_bps,
bit_depth,
)
.map(|e| Box::new(e) as Box<dyn Encoder>)
}
#[cfg(not(feature = "amf-qsv"))]
{
let _ = backend;
anyhow::bail!(
"AMD/Intel (AMF/QSV) encode requested/detected but this host was built \
without it — rebuild with `--features amf-qsv` (needs ffmpeg-next + a \
FFMPEG_DIR with the AMF/QSV encoders at build time)"
)
}
}
WindowsBackend::Software => {
anyhow::ensure!(
codec == Codec::H264,
"the Windows software encoder supports H.264 only; client negotiated {codec:?} \
(build a GPU backend: --features nvenc or amf-qsv, or request H264)"
);
let _ = bit_depth; // the software H.264 path is 8-bit only
// Software H.264 realistically caps far below the negotiated hardware rates.
const SW_BITRATE_CEIL: u64 = 100_000_000;
sw::OpenH264Encoder::open(
format, format,
width, width,
height, height,
fps, fps,
bitrate_bps, bitrate_bps.min(SW_BITRATE_CEIL),
bit_depth, )
)?; .map(|e| Box::new(e) as Box<dyn Encoder>)
return Ok(Box::new(enc) as Box<dyn Encoder>);
}
#[cfg(not(feature = "nvenc"))]
{
anyhow::bail!(
"NVENC requested but this host was built without it — rebuild with \
`--features nvenc` (needs the NVENC SDK's nvencodeapi.lib at link time)"
);
} }
} }
anyhow::ensure!(
codec == Codec::H264,
"the Windows software encoder supports H.264 only; client negotiated {codec:?} \
(set PUNKTFUNK_ENCODER=nvenc for a GPU host, or request H264)"
);
// Software H.264 realistically caps far below the negotiated hardware rates.
const SW_BITRATE_CEIL: u64 = 100_000_000;
let enc = sw::OpenH264Encoder::open(
format,
width,
height,
fps,
bitrate_bps.min(SW_BITRATE_CEIL),
)?;
Ok(Box::new(enc) as Box<dyn Encoder>)
} }
#[cfg(not(any(target_os = "linux", target_os = "windows")))] #[cfg(not(any(target_os = "linux", target_os = "windows")))]
{ {
@@ -339,7 +393,7 @@ pub fn linux_zero_copy_is_vaapi() -> bool {
/// Which codecs the active GPU can actually ENCODE. Used to build the GameStream codec /// Which codecs the active GPU can actually ENCODE. Used to build the GameStream codec
/// advertisement so a client never negotiates a codec the GPU can't do (AV1 encode is narrow — /// advertisement so a client never negotiates a codec the GPU can't do (AV1 encode is narrow —
/// Intel Arc/Xe2+, AMD RDNA3+/RDNA4 — so it must be probed, not assumed). /// Intel Arc/Xe2+, AMD RDNA3+/RDNA4 — so it must be probed, not assumed).
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", target_os = "windows"))]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct CodecSupport { pub struct CodecSupport {
pub h264: bool, pub h264: bool,
@@ -370,6 +424,123 @@ pub fn vaapi_codec_support() -> CodecSupport {
}) })
} }
// ---------------------------------------------------------------------------------------------
// Windows backend selection (the analogue of the Linux nvidia_present / linux_zero_copy_is_vaapi
// logic). NVIDIA → NVENC, AMD → AMF, Intel → QSV; `auto` (default) reads the DXGI adapter vendor.
// ---------------------------------------------------------------------------------------------
#[cfg(target_os = "windows")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WindowsBackend {
Nvenc,
Amf,
Qsv,
Software,
}
#[cfg(target_os = "windows")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum GpuVendor {
Nvidia,
Amd,
Intel,
}
/// Resolve the active Windows encode backend from `PUNKTFUNK_ENCODER` (`auto` → the DXGI adapter
/// vendor). Shared by [`open_video`] and the GameStream codec advertisement so both agree.
#[cfg(target_os = "windows")]
pub(crate) fn windows_resolved_backend() -> WindowsBackend {
let pref = std::env::var("PUNKTFUNK_ENCODER")
.unwrap_or_default()
.to_ascii_lowercase();
match pref.as_str() {
"nvenc" | "hw" | "nvidia" | "cuda" => WindowsBackend::Nvenc,
"amf" | "amd" => WindowsBackend::Amf,
"qsv" | "intel" => WindowsBackend::Qsv,
"sw" | "software" | "openh264" => WindowsBackend::Software,
_ => match windows_gpu_vendor() {
Some(GpuVendor::Nvidia) => WindowsBackend::Nvenc,
Some(GpuVendor::Amd) => WindowsBackend::Amf,
Some(GpuVendor::Intel) => WindowsBackend::Qsv,
None => WindowsBackend::Software,
},
}
}
/// True if the active Windows backend is the libavcodec AMF/QSV path (so the codec advertisement
/// consults a real GPU probe rather than the NVENC static superset). Always false when the
/// `amf-qsv` feature is off — there's then no ffmpeg backend to probe.
#[cfg(target_os = "windows")]
pub fn windows_backend_is_ffmpeg() -> bool {
cfg!(feature = "amf-qsv")
&& matches!(
windows_resolved_backend(),
WindowsBackend::Amf | WindowsBackend::Qsv
)
}
/// Detect the host GPU vendor from the first hardware DXGI adapter (Windows has no `/dev/nvidia*`
/// probe). Cached. NVIDIA=0x10DE, AMD=0x1002, Intel=0x8086; the software/WARP adapter is skipped.
#[cfg(target_os = "windows")]
fn windows_gpu_vendor() -> Option<GpuVendor> {
use std::sync::OnceLock;
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, IDXGIFactory1, DXGI_ADAPTER_FLAG_SOFTWARE,
};
static CACHE: OnceLock<Option<GpuVendor>> = OnceLock::new();
*CACHE.get_or_init(|| unsafe {
let factory: IDXGIFactory1 = CreateDXGIFactory1().ok()?;
let mut i = 0u32;
while let Ok(adapter) = factory.EnumAdapters1(i) {
i += 1;
// windows-rs 0.62: GetDesc1 returns the desc by value (no out-param).
let Ok(desc) = adapter.GetDesc1() else {
continue;
};
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0 {
continue; // skip the Microsoft Basic Render / WARP adapter
}
match desc.VendorId {
0x10DE => return Some(GpuVendor::Nvidia),
0x1002 => return Some(GpuVendor::Amd),
0x8086 => return Some(GpuVendor::Intel),
_ => continue,
}
}
None
})
}
/// Probe the active Windows AMF/QSV backend for its encodable codecs (cached; opens a tiny encoder
/// per codec, once). Mirrors [`vaapi_codec_support`]; called only when [`windows_backend_is_ffmpeg`]
/// is true. AV1 is narrow (AMD RDNA3+, Intel Arc/Xe2+), so it must be probed, not assumed.
#[cfg(all(target_os = "windows", feature = "amf-qsv"))]
pub fn windows_codec_support() -> CodecSupport {
use std::sync::OnceLock;
static CACHE: OnceLock<CodecSupport> = OnceLock::new();
*CACHE.get_or_init(|| {
let vendor = match windows_resolved_backend() {
WindowsBackend::Qsv => ffmpeg_win::WinVendor::Qsv,
_ => ffmpeg_win::WinVendor::Amf,
};
let caps = CodecSupport {
h264: ffmpeg_win::probe_can_encode(vendor, Codec::H264),
h265: ffmpeg_win::probe_can_encode(vendor, Codec::H265),
av1: ffmpeg_win::probe_can_encode(vendor, Codec::Av1),
};
tracing::info!(
backend = ?vendor,
h264 = caps.h264,
h265 = caps.h265,
av1 = caps.av1,
"Windows AMF/QSV encode capabilities probed"
);
caps
})
}
#[cfg(all(target_os = "windows", feature = "amf-qsv"))]
mod ffmpeg_win;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod linux; mod linux;
#[cfg(all(target_os = "windows", feature = "nvenc"))] #[cfg(all(target_os = "windows", feature = "nvenc"))]
File diff suppressed because it is too large Load Diff
@@ -50,29 +50,41 @@ pub fn serverinfo_xml(host: &Host, https: bool, paired: bool) -> String {
fn codec_mode_support() -> u32 { fn codec_mode_support() -> u32 {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if crate::encode::linux_zero_copy_is_vaapi() { if crate::encode::linux_zero_copy_is_vaapi() {
use super::{SCM_AV1_MAIN8, SCM_H264, SCM_HEVC}; if let Some(m) = probed_mask(crate::encode::vaapi_codec_support()) {
let caps = crate::encode::vaapi_codec_support(); return m;
let mut m = 0;
if caps.h264 {
m |= SCM_H264;
} }
if caps.h265 { }
m |= SCM_HEVC; // Windows AMD/Intel (AMF/QSV): advertise only what the GPU actually encodes (AV1 is narrow, an
} // old iGPU might lack HEVC). NVENC and the GPU-less software path keep the static superset.
if caps.av1 { #[cfg(all(target_os = "windows", feature = "amf-qsv"))]
m |= SCM_AV1_MAIN8; if crate::encode::windows_backend_is_ffmpeg() {
} if let Some(m) = probed_mask(crate::encode::windows_codec_support()) {
// Only trust a probe that actually found an encoder. An empty result means VAAPI wasn't
// usable at probe time (no VA display — a GPU-less CI box, or a misconfigured host), NOT
// that the GPU encodes nothing; advertise the static superset (pre-probe behaviour) rather
// than claiming zero codecs.
if m != 0 {
return m; return m;
} }
} }
SERVER_CODEC_MODE_SUPPORT SERVER_CODEC_MODE_SUPPORT
} }
/// Turn a probed [`CodecSupport`](crate::encode::CodecSupport) into a `ServerCodecModeSupport` mask,
/// or `None` if the probe found nothing — meaning the GPU wasn't usable at probe time (GPU-less CI,
/// a misconfigured/wrong-vendor host), NOT that it encodes zero codecs; the caller then advertises
/// the static superset (pre-probe behaviour) rather than claiming nothing.
#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "amf-qsv")))]
fn probed_mask(caps: crate::encode::CodecSupport) -> Option<u32> {
use super::{SCM_AV1_MAIN8, SCM_H264, SCM_HEVC};
let mut m = 0;
if caps.h264 {
m |= SCM_H264;
}
if caps.h265 {
m |= SCM_HEVC;
}
if caps.av1 {
m |= SCM_AV1_MAIN8;
}
(m != 0).then_some(m)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+6 -3
View File
@@ -603,7 +603,8 @@ fn uninstall() -> Result<()> {
Ok(()) Ok(())
} }
/// Write a default `host.env` if none exists, so a fresh install streams with NVENC out of the box. /// Write a default `host.env` if none exists, so a fresh install streams out of the box. The encoder
/// defaults to `auto` — the host picks NVENC (NVIDIA) / AMF (AMD) / QSV (Intel) from the GPU vendor.
fn ensure_default_host_env() -> Result<()> { fn ensure_default_host_env() -> Result<()> {
let path = host_env_path(); let path = host_env_path();
if path.exists() { if path.exists() {
@@ -616,7 +617,9 @@ fn ensure_default_host_env() -> Result<()> {
# KEY=VALUE per line; '#' comments. Restart the service after editing:\n\ # KEY=VALUE per line; '#' comments. Restart the service after editing:\n\
# punktfunk-host service stop && punktfunk-host service start\n\ # punktfunk-host service stop && punktfunk-host service start\n\
\n\ \n\
PUNKTFUNK_ENCODER=nvenc\n\ # Encode backend: auto (default) detects the GPU vendor — NVIDIA->nvenc, AMD->amf, Intel->qsv.\n\
# Force one with nvenc | amf | qsv | sw (software H.264). amf/qsv need an FFmpeg-built host.\n\
PUNKTFUNK_ENCODER=auto\n\
PUNKTFUNK_VIDEO_SOURCE=virtual\n\ PUNKTFUNK_VIDEO_SOURCE=virtual\n\
PUNKTFUNK_SECURE_DDA=1\n\ PUNKTFUNK_SECURE_DDA=1\n\
RUST_LOG=info\n\ RUST_LOG=info\n\
@@ -625,7 +628,7 @@ fn ensure_default_host_env() -> Result<()> {
# compat). Use `serve` for a SECURE native-only host (no GameStream #5/#9 surface).\n\ # compat). Use `serve` for a SECURE native-only host (no GameStream #5/#9 surface).\n\
# PUNKTFUNK_HOST_CMD=serve --gamestream\n\ # PUNKTFUNK_HOST_CMD=serve --gamestream\n\
\n\ \n\
# Force a specific NVENC render GPU by name substring (multi-GPU boxes only):\n\ # Force a specific render GPU by name substring (multi-GPU boxes only):\n\
# PUNKTFUNK_RENDER_ADAPTER=4090\n"; # PUNKTFUNK_RENDER_ADAPTER=4090\n";
std::fs::write(&path, default).with_context(|| format!("write {}", path.display()))?; std::fs::write(&path, default).with_context(|| format!("write {}", path.display()))?;
println!("Wrote default config: {}", path.display()); println!("Wrote default config: {}", path.display());
+9 -6
View File
@@ -143,12 +143,15 @@ rustc 1.96 clippy is stricter than the Linux CI image on shared code, e.g. `need
driver DLL — `lib /def:nvenc.def /machine:x64 /out:nvencodeapi.lib` where `nvenc.def` lists driver DLL — `lib /def:nvenc.def /machine:x64 /out:nvencodeapi.lib` where `nvenc.def` lists
`NvEncodeAPICreateInstance` and `NvEncodeAPIGetMaxSupportedVersion` — and set `NvEncodeAPICreateInstance` and `NvEncodeAPIGetMaxSupportedVersion` — and set
`PUNKTFUNK_NVENC_LIB_DIR` to its directory. `PUNKTFUNK_NVENC_LIB_DIR` to its directory.
3. `cargo build -p punktfunk-host --features nvenc` (needs NASM + CMake for aws-lc-rs; libclang for 3. `cargo build -p punktfunk-host --features nvenc,amf-qsv` for the all-vendor GPU host (NVENC for
any ffmpeg-using client). Default build (no feature) uses the openh264 software encoder. NVIDIA; AMD AMF + Intel QSV via libavcodec — `amf-qsv` needs `FFMPEG_DIR` with the `*_amf`/`*_qsv`
encoders at build, e.g. the BtbN gpl-shared tree, and the FFmpeg DLLs on PATH at run; NASM + CMake
for aws-lc-rs; libclang for ffmpeg-sys-next). Default build (no feature) = openh264 software encoder.
4. Run in the **interactive session** (not a Session-0 service / not over SSH — SendInput + DXGI 4. Run in the **interactive session** (not a Session-0 service / not over SSH — SendInput + DXGI
Desktop Duplication need a desktop): `serve` or `punktfunk1-host --source virtual`. Set Desktop Duplication need a desktop): `serve` or `punktfunk1-host --source virtual`.
`PUNKTFUNK_ENCODER=nvenc` to select NVENC (the DXGI capturer switches to zero-copy D3D11 output to `PUNKTFUNK_ENCODER=auto` (default) picks the backend from the GPU vendor — `nvenc`/`amf`/`qsv`/`sw`
match). The SudoVDA monitor activates once a real GPU drives WDDM, so capture + NVENC then work. force one. The DXGI capturer emits zero-copy D3D11 NV12/P010 to match any GPU backend; the SudoVDA
monitor activates once a real GPU drives WDDM, so capture + encode then work.
### Dev loop (this repo → the Windows VM) ### Dev loop (this repo → the Windows VM)
@@ -224,7 +227,7 @@ This is the highest-value first move and is **fully doable GPU-less**.
| **VirtualDisplay** | KWin/gamescope/Mutter/Sway | **SudoVDA** IOCTLs (below) + `SetDisplayConfig` mode-set | ✅ likely (WARP) — *spike* | | **VirtualDisplay** | KWin/gamescope/Mutter/Sway | **SudoVDA** IOCTLs (below) + `SetDisplayConfig` mode-set | ✅ likely (WARP) — *spike* |
| **Capture** | PipeWire/dmabuf | **DXGI Desktop Duplication** primary, **WGC** fallback → `ID3D11Texture2D`; add `FramePayload::D3d11` | ⚠️ DDA-on-WARP unreliable; WGC-on-WARP unverified — *spike* | | **Capture** | PipeWire/dmabuf | **DXGI Desktop Duplication** primary, **WGC** fallback → `ID3D11Texture2D`; add `FramePayload::D3d11` | ⚠️ DDA-on-WARP unreliable; WGC-on-WARP unverified — *spike* |
| **Zero-copy** | dmabuf→EGL/Vulkan→CUDA | register `ID3D11Texture2D` with NVENC (`NV_ENC_DEVICE_TYPE_DIRECTX`) — no CUDA bridge | ❌ needs real GPU | | **Zero-copy** | dmabuf→EGL/Vulkan→CUDA | register `ID3D11Texture2D` with NVENC (`NV_ENC_DEVICE_TYPE_DIRECTX`) — no CUDA bridge | ❌ needs real GPU |
| **Encode** | ffmpeg `*_nvenc` | `openh264` SW (default on VM) + `nvidia-video-codec-sdk` HW (real GPU); behind `PUNKTFUNK_ENCODER` | SW ✅ / HW ❌ | | **Encode** | ffmpeg `*_nvenc`/`*_vaapi` | `nvidia-video-codec-sdk` (NVIDIA) + libavcodec `*_amf`/`*_qsv` (AMD/Intel, `encode/ffmpeg_win.rs`, `--features amf-qsv`) + `openh264` SW fallback; vendor-auto via `PUNKTFUNK_ENCODER` | NVENC ✅ live / AMF/QSV CI-only |
| **Input kbd/mouse** | libei / wlr | **SendInput** with `MOUSEEVENTF_VIRTUALDESK` absolute mapping onto the virtual desktop rect (skip the VK→evdev table — client sends Win VKs; use `KEYEVENTF_SCANCODE`+`EXTENDEDKEY`) | ✅ | | **Input kbd/mouse** | libei / wlr | **SendInput** with `MOUSEEVENTF_VIRTUALDESK` absolute mapping onto the virtual desktop rect (skip the VK→evdev table — client sends Win VKs; use `KEYEVENTF_SCANCODE`+`EXTENDEDKEY`) | ✅ |
| **Gamepad** | uinput xpad + FF | **ViGEmBus** via `vigem-client` (`Xbox360Wired`); rumble via `request_notification()``XNotification{large,small}` | ✅ (install driver) | | **Gamepad** | uinput xpad + FF | **ViGEmBus** via `vigem-client` (`Xbox360Wired`); rumble via `request_notification()``XNotification{large,small}` | ✅ (install driver) |
| **Audio capture** | PipeWire sink monitor | **WASAPI loopback** via the `wasapi` crate (48 kHz stereo f32 → existing Opus) | ⚠️ needs an audio endpoint | | **Audio capture** | PipeWire sink monitor | **WASAPI loopback** via the `wasapi` crate (48 kHz stereo f32 → existing Opus) | ⚠️ needs an audio endpoint |
+19
View File
@@ -25,6 +25,7 @@ param(
[string]$Publisher = 'CN=unom', [string]$Publisher = 'CN=unom',
[string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # reuse the client's signing secret [string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # reuse the client's signing secret
[string]$PfxPassword = $env:MSIX_CERT_PASSWORD, [string]$PfxPassword = $env:MSIX_CERT_PASSWORD,
[string]$FfmpegDir = $env:FFMPEG_DIR, # bundle its bin\*.dll (amf-qsv build)
[switch]$NoDriver, # build without the bundled SudoVDA driver [switch]$NoDriver, # build without the bundled SudoVDA driver
[switch]$NoSign # skip signing (local debug) [switch]$NoSign # skip signing (local debug)
) )
@@ -146,6 +147,24 @@ if (-not $NoDriver) {
} }
else { Write-Host "-NoDriver: building installer WITHOUT the bundled SudoVDA driver" } else { Write-Host "-NoDriver: building installer WITHOUT the bundled SudoVDA driver" }
# --- stage the FFmpeg shared DLLs (AMD/Intel AMF/QSV build) ------------------------------------
# A host built with --features amf-qsv link-imports avcodec/avutil/swscale/... so the shared DLLs
# MUST sit next to the exe (it won't start otherwise). Bundle them from $FfmpegDir\bin — the same
# BtbN gpl-shared tree the build linked against. A nvenc/software-only build doesn't import them, so
# this is a harmless extra there; skipped entirely when $FfmpegDir is unset.
$ffmpegBinSrc = if ($FfmpegDir) { Join-Path $FfmpegDir 'bin' } else { $null }
if ($ffmpegBinSrc -and (Test-Path $ffmpegBinSrc)) {
$dlls = Get-ChildItem -Path $ffmpegBinSrc -Filter '*.dll' -ErrorAction SilentlyContinue
if ($dlls) {
$ffmpegStage = Join-Path $OutDir 'ffmpeg'
New-Item -ItemType Directory -Force -Path $ffmpegStage | Out-Null
$dlls | ForEach-Object { Copy-Item $_.FullName -Destination $ffmpegStage -Force }
$defines += "/DFfmpegBin=$ffmpegStage"
Write-Host "bundling $($dlls.Count) FFmpeg DLL(s) from $ffmpegBinSrc"
}
}
else { Write-Host "no FFMPEG_DIR\bin -> installer built WITHOUT FFmpeg DLLs (nvenc/software-only host)" }
# --- build the installer (from the non-redirected copy under C:\t) ----------------------------- # --- build the installer (from the non-redirected copy under C:\t) -----------------------------
Write-Host "==> ISCC $($defines -join ' ') $issLocal" Write-Host "==> ISCC $($defines -join ' ') $issLocal"
& $iscc @defines $issLocal & $iscc @defines $issLocal
+11
View File
@@ -32,6 +32,11 @@
#ifdef StageDir #ifdef StageDir
#define WithDriver #define WithDriver
#endif #endif
; FfmpegBin (a dir of FFmpeg shared DLLs) is optional — present when the host is built with
; --features amf-qsv (the AMD/Intel AMF/QSV encode backend link-imports the FFmpeg libs).
#ifdef FfmpegBin
#define WithFfmpeg
#endif
[Setup] [Setup]
AppId={{7C9E6A52-1F4B-4E8D-A3C7-2B5D8F1E0A93} AppId={{7C9E6A52-1F4B-4E8D-A3C7-2B5D8F1E0A93}
@@ -68,6 +73,12 @@ Name: "startservice"; Description: "Start the punktfunk host service now (also s
Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#HostEnv}"; DestDir: "{app}"; Flags: ignoreversion Source: "{#HostEnv}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion
#ifdef WithFfmpeg
; FFmpeg shared DLLs (avcodec/avutil/swscale/...) laid down next to the exe — the AMD/Intel
; (AMF/QSV) encode backend link-imports them, so the exe won't start without them. NVENC/software-
; only builds simply omit this block.
Source: "{#FfmpegBin}\*.dll"; DestDir: "{app}"; Flags: ignoreversion
#endif
#ifdef WithDriver #ifdef WithDriver
; The driver payload + nefconc.exe + install-sudovda.ps1, extracted to {tmp} and removed after install. ; The driver payload + nefconc.exe + install-sudovda.ps1, extracted to {tmp} and removed after install.
Source: "{#StageDir}\*"; DestDir: "{tmp}\sudovda"; Flags: deleteafterinstall recursesubdirs createallsubdirs; Tasks: installdriver Source: "{#StageDir}\*"; DestDir: "{tmp}\sudovda"; Flags: deleteafterinstall recursesubdirs createallsubdirs; Tasks: installdriver
+5 -3
View File
@@ -9,9 +9,11 @@
# Format: KEY=VALUE per line; '#' starts a comment. The service loads these into its environment # Format: KEY=VALUE per line; '#' starts a comment. The service loads these into its environment
# and passes PUNKTFUNK_* and RUST_LOG through to the host it launches into the active session. # and passes PUNKTFUNK_* and RUST_LOG through to the host it launches into the active session.
# Hardware encode via NVENC (NVIDIA). The host must be the `--features nvenc` build. Falls back to # Hardware encode backend. `auto` (default) detects the GPU vendor: NVIDIA->nvenc (direct SDK),
# the software encoder automatically if NVENC is unavailable. # AMD->amf, Intel->qsv (both libavcodec). Force one with: nvenc | amf | qsv | sw (software H.264).
PUNKTFUNK_ENCODER=nvenc # nvenc needs the `--features nvenc` build; amf/qsv need the `--features amf-qsv` build (FFmpeg DLLs
# ship in the installer). The published installer is built with all three.
PUNKTFUNK_ENCODER=auto
# Video source: `virtual` creates a per-client virtual display (SudoVDA) at the client's exact # Video source: `virtual` creates a per-client virtual display (SudoVDA) at the client's exact
# resolution + refresh — the flagship mode. Requires the SudoVDA indirect display driver installed. # resolution + refresh — the flagship mode. Requires the SudoVDA indirect display driver installed.