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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,16 +218,15 @@ 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")]
|
||||||
{
|
{
|
||||||
let enc = nvenc::NvencD3d11Encoder::open(
|
nvenc::NvencD3d11Encoder::open(
|
||||||
codec,
|
codec,
|
||||||
format,
|
format,
|
||||||
width,
|
width,
|
||||||
@@ -215,32 +234,67 @@ pub fn open_video(
|
|||||||
fps,
|
fps,
|
||||||
bitrate_bps,
|
bitrate_bps,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
)?;
|
)
|
||||||
return Ok(Box::new(enc) as Box<dyn Encoder>);
|
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "nvenc"))]
|
#[cfg(not(feature = "nvenc"))]
|
||||||
{
|
{
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"NVENC requested but this host was built without it — rebuild with \
|
"NVENC requested/detected but this host was built without it — rebuild \
|
||||||
`--features nvenc` (needs the NVENC SDK's nvencodeapi.lib at link time)"
|
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!(
|
anyhow::ensure!(
|
||||||
codec == Codec::H264,
|
codec == Codec::H264,
|
||||||
"the Windows software encoder supports H.264 only; client negotiated {codec:?} \
|
"the Windows software encoder supports H.264 only; client negotiated {codec:?} \
|
||||||
(set PUNKTFUNK_ENCODER=nvenc for a GPU host, or request H264)"
|
(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.
|
// Software H.264 realistically caps far below the negotiated hardware rates.
|
||||||
const SW_BITRATE_CEIL: u64 = 100_000_000;
|
const SW_BITRATE_CEIL: u64 = 100_000_000;
|
||||||
let enc = sw::OpenH264Encoder::open(
|
sw::OpenH264Encoder::open(
|
||||||
format,
|
format,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fps,
|
fps,
|
||||||
bitrate_bps.min(SW_BITRATE_CEIL),
|
bitrate_bps.min(SW_BITRATE_CEIL),
|
||||||
)?;
|
)
|
||||||
Ok(Box::new(enc) as Box<dyn Encoder>)
|
.map(|e| Box::new(e) 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,8 +50,28 @@ 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() {
|
||||||
|
if let Some(m) = probed_mask(crate::encode::vaapi_codec_support()) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
#[cfg(all(target_os = "windows", feature = "amf-qsv"))]
|
||||||
|
if crate::encode::windows_backend_is_ffmpeg() {
|
||||||
|
if let Some(m) = probed_mask(crate::encode::windows_codec_support()) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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};
|
use super::{SCM_AV1_MAIN8, SCM_H264, SCM_HEVC};
|
||||||
let caps = crate::encode::vaapi_codec_support();
|
|
||||||
let mut m = 0;
|
let mut m = 0;
|
||||||
if caps.h264 {
|
if caps.h264 {
|
||||||
m |= SCM_H264;
|
m |= SCM_H264;
|
||||||
@@ -62,15 +82,7 @@ fn codec_mode_support() -> u32 {
|
|||||||
if caps.av1 {
|
if caps.av1 {
|
||||||
m |= SCM_AV1_MAIN8;
|
m |= SCM_AV1_MAIN8;
|
||||||
}
|
}
|
||||||
// Only trust a probe that actually found an encoder. An empty result means VAAPI wasn't
|
(m != 0).then_some(m)
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SERVER_CODEC_MODE_SUPPORT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user