feat(clients/windows): port the Vulkan session client to Windows — session-always
The punktfunk-session Vulkan client (clients/linux-session, now clients/session) builds and runs on Windows; the WinUI shell spawns it for every stream. Verified live: 10-bit HEVC via Vulkan Video on both AMD (iGPU) and NVIDIA, 5120x1440 at 130 fps / 8 ms end-to-end on the RTX 4090. - pf-ffvk: Windows bindgen branch (FFMPEG_DIR + PF_FFVK_VULKAN_INCLUDE, no pkg-config); provisioning fetches Vulkan-Headers (pinned v1.4.309). - pf-client-core: builds on Windows — WASAPI audio (audio_wasapi.rs, cfg-swapped via #[path], same surface as the PipeWire twin), VAAPI/dmabuf gated inline (chain = vulkan -> software), trust reads the WinUI shell's %APPDATA% stores (parity tests pin both serialized shapes), Settings gains adapter/hdr_enabled (serde-defaulted; Linux stores unaffected). - pf-presenter: builds on Windows — dmabuf module Linux-gated; SDL keyboard grab while captured (Alt+Tab/Win reach the host); pick_device ranks discrete over integrated (device 0 was the iGPU on hybrid boxes — the silent footgun) and honors PUNKTFUNK_VK_ADAPTER (the Settings GPU pick, exported by the session). - run loop: block in one SDL wait woken by input AND decoded frames (a per- session forwarder pushes a FrameWake user event) instead of a 1 ms poll — measured 111%% -> 5%% of a core (NVIDIA), 86%% -> 3.5%% (AMD), stats unchanged. The pump's decode-fence wait became once-per-window sampling (no per-frame pipeline stall; the stat now shows true backlog). - pf-console-ui: builds on Windows (skia-safe msvc prebuilts); font lookup falls through fontconfig aliases to concrete DirectWrite families (Consolas/Segoe UI) — browse/coverflow works, verified against a live host. - WinUI shell: session-always via new src/spawn.rs (GTK spawn.rs port — CREATE_NO_WINDOW, stdout contract, kill handle); the Stream screen is a status card (chips + stage lines from the child's stats). The legacy in-process D3D11VA path stays behind Settings "Streaming engine" / PUNKTFUNK_BUILTIN_ STREAM=1 as the A/B baseline until Phase 8 deletes it. SessionParams.video_caps makes the HDR toggle real. - clients/linux-session renamed to clients/session (builds for both OSes). - CI/MSIX: both workflows build/test both bins with widened path filters; the MSIX ships punktfunk-session.exe. ARM64 session builds --no-default-features (rust-skia has no aarch64-pc-windows-msvc prebuilts; flip when it does). A/B on this box (5120x1440 HEVC vs home-worker-5): NVIDIA Vulkan 130 fps / 8 ms e2e / 1.6 ms decode — clearly better than the built-in path. The AMD iGPU VCN saturates at ~52 fps where its own D3D11VA does ~70 — Adrenalin Vulkan decode is slower on APU silicon; discrete RDNA validation gates Phase 8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
//! Client identity, the known-hosts (pinned fingerprint) store, and app settings.
|
||||
//!
|
||||
//! The identity shares `~/.config/punktfunk/client-{cert,key}.pem` with `punktfunk-probe`
|
||||
//! so a box pairs once whichever client it uses.
|
||||
//! The identity shares `~/.config/punktfunk/client-{cert,key}.pem` (Linux; on Windows
|
||||
//! `%APPDATA%\punktfunk`, the WinUI shell's directory) with `punktfunk-probe` so a box
|
||||
//! pairs once whichever client it uses. On Windows the session binary reads the SAME
|
||||
//! stores the WinUI shell (`clients/windows/src/trust.rs`) writes — pairing there makes
|
||||
//! the session connect silently, mirroring the GTK-shell arrangement on Linux. The two
|
||||
//! `Settings` structs differ in shape; `#[serde(default)]` on both sides reconciles them
|
||||
//! (see the parity tests below), and the shell stays the settings file's only writer.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use punktfunk_core::client::NativeClient;
|
||||
@@ -10,8 +15,16 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn config_dir() -> Result<PathBuf> {
|
||||
let home = std::env::var("HOME").context("HOME unset")?;
|
||||
Ok(PathBuf::from(home).join(".config/punktfunk"))
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let appdata = std::env::var("APPDATA").context("APPDATA unset")?;
|
||||
Ok(PathBuf::from(appdata).join("punktfunk"))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let home = std::env::var("HOME").context("HOME unset")?;
|
||||
Ok(PathBuf::from(home).join(".config/punktfunk"))
|
||||
}
|
||||
}
|
||||
|
||||
/// This client's persistent identity, generated on first use — presented on every connect
|
||||
@@ -256,6 +269,18 @@ pub struct Settings {
|
||||
/// `"vulkan"`, `"vaapi"`, `"software"`.
|
||||
/// The `PUNKTFUNK_DECODER` env var overrides this (see `video::Decoder::new`).
|
||||
pub decoder: String,
|
||||
/// Decode/present GPU (multi-GPU boxes): the adapter's marketing name, as the WinUI
|
||||
/// shell's GPU picker stores it; empty = automatic. The session maps it onto the
|
||||
/// presenter's device pick (`PUNKTFUNK_VK_ADAPTER`). `default` so pre-existing
|
||||
/// stores (and the Linux shells, which have no picker yet) load.
|
||||
#[serde(default)]
|
||||
pub adapter: String,
|
||||
/// Advertise 10-bit + HDR10 so the host upgrades HDR content to a Main10/PQ stream.
|
||||
/// The presenter handles the display side dynamically either way (HDR10 swapchain
|
||||
/// where offered, tonemap where not) — off means "never send me 10-bit".
|
||||
/// `default = true`: the Linux stores never carried this and always advertised.
|
||||
#[serde(default = "default_true")]
|
||||
pub hdr_enabled: bool,
|
||||
/// Show the on-stream statistics overlay (toggle live with Ctrl+Alt+Shift+S).
|
||||
pub show_stats: bool,
|
||||
/// Enter fullscreen when a stream starts (F11 / the controller chord / the top-edge
|
||||
@@ -270,6 +295,10 @@ fn default_codec() -> String {
|
||||
"auto".into()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto).
|
||||
pub fn preferred_codec(&self) -> u8 {
|
||||
@@ -297,6 +326,8 @@ impl Default for Settings {
|
||||
audio_channels: 2,
|
||||
codec: "auto".into(),
|
||||
decoder: "auto".into(),
|
||||
adapter: String::new(),
|
||||
hdr_enabled: true,
|
||||
show_stats: true,
|
||||
fullscreen_on_stream: true,
|
||||
library_enabled: false,
|
||||
@@ -306,6 +337,13 @@ impl Default for Settings {
|
||||
|
||||
impl Settings {
|
||||
fn path() -> Result<PathBuf> {
|
||||
// The shell's settings file on each OS: the GTK shell's on Linux, the WinUI
|
||||
// shell's on Windows. The shells own (and write) these files; the session binary
|
||||
// only reads them, so `save` must never be called on Windows — it would rewrite
|
||||
// the file in THIS struct's shape and drop the WinUI-only fields.
|
||||
#[cfg(windows)]
|
||||
return Ok(config_dir()?.join("client-windows-settings.json"));
|
||||
#[cfg(not(windows))]
|
||||
Ok(config_dir()?.join("client-gtk-settings.json"))
|
||||
}
|
||||
|
||||
@@ -340,4 +378,52 @@ mod tests {
|
||||
let round: Settings = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
|
||||
assert_eq!(round.forward_pad, "");
|
||||
}
|
||||
|
||||
/// On Windows the session reads the WinUI shell's settings file. This fixture is the
|
||||
/// shell's `Settings` shape (clients/windows/src/trust.rs) verbatim — if that struct
|
||||
/// changes, update this fixture with it. WinUI-only fields (hdr_enabled, adapter,
|
||||
/// show_hud) must be ignored; fields this struct has and the shell's lacks
|
||||
/// (forward_pad, show_stats, …) must default; the shell's D3D11VA-era
|
||||
/// `decoder: "hardware"` must survive as-is (video::Decoder::new reads it as auto).
|
||||
#[test]
|
||||
fn settings_reads_winui_shell_shape() {
|
||||
let shell = r#"{
|
||||
"width": 2560, "height": 1440, "refresh_hz": 120, "bitrate_kbps": 20000,
|
||||
"gamepad": "dualsense", "compositor": "auto",
|
||||
"inhibit_shortcuts": true, "mic_enabled": true, "audio_channels": 6,
|
||||
"hdr_enabled": true, "decoder": "hardware", "codec": "av1",
|
||||
"adapter": "NVIDIA GeForce RTX 4080", "show_hud": false
|
||||
}"#;
|
||||
let s: Settings = serde_json::from_str(shell).unwrap();
|
||||
assert_eq!((s.width, s.height, s.refresh_hz), (2560, 1440, 120));
|
||||
assert_eq!(s.bitrate_kbps, 20000);
|
||||
assert_eq!(s.audio_channels, 6);
|
||||
assert!(s.mic_enabled);
|
||||
assert_eq!(s.decoder, "hardware");
|
||||
assert_eq!(s.preferred_codec(), punktfunk_core::quic::CODEC_AV1);
|
||||
assert_eq!(s.adapter, "NVIDIA GeForce RTX 4080");
|
||||
assert!(s.hdr_enabled);
|
||||
// Fields the shell's file doesn't carry take this struct's defaults.
|
||||
assert_eq!(s.forward_pad, "");
|
||||
assert!(s.show_stats);
|
||||
assert!(s.fullscreen_on_stream);
|
||||
assert!(!s.library_enabled);
|
||||
}
|
||||
|
||||
/// The WinUI shell's known-hosts shape (no `last_used` field) loads losslessly — same
|
||||
/// filename, same directory, so on Windows the two clients genuinely share the store.
|
||||
#[test]
|
||||
fn known_hosts_reads_winui_shell_shape() {
|
||||
let shell = r#"{"hosts":[{
|
||||
"name": "Gaming PC", "addr": "192.168.1.50", "port": 9777,
|
||||
"fp_hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"paired": true, "mac": ["aa:bb:cc:dd:ee:ff"]
|
||||
}]}"#;
|
||||
let k: KnownHosts = serde_json::from_str(shell).unwrap();
|
||||
let h = k.find_by_addr("192.168.1.50", 9777).unwrap();
|
||||
assert!(h.paired);
|
||||
assert_eq!(h.last_used, None);
|
||||
assert_eq!(h.mac, vec!["aa:bb:cc:dd:ee:ff".to_string()]);
|
||||
assert!(parse_hex32(&h.fp_hex).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user