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:
@@ -18,7 +18,7 @@ use windows_reactor::*;
|
||||
/// One HUD refresh: the latest session stats, the input hooks' capture state, and the render
|
||||
/// thread's display-side window. Mirrored into root state by the poll thread (`pf-hud`) and
|
||||
/// passed down as a prop.
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub(crate) struct HudSample {
|
||||
pub(crate) stats: Stats,
|
||||
pub(crate) captured: bool,
|
||||
@@ -30,6 +30,9 @@ pub(crate) struct HudSample {
|
||||
/// The render thread's glass-side window (presents/s, skips, end-to-end p50/p95, display
|
||||
/// stage p50) — see [`crate::render::present_stats`].
|
||||
pub(crate) present: crate::render::PresentStats,
|
||||
/// Spawn mode: the session child's latest formatted `stats:` line, for the status
|
||||
/// page. Empty in builtin mode / before the first window.
|
||||
pub(crate) stats_line: String,
|
||||
}
|
||||
|
||||
/// Props for the stream page: the services plus the live HUD sample that drives the overlay
|
||||
@@ -155,6 +158,108 @@ pub(crate) fn stream_page(props: &StreamProps, cx: &mut RenderCx) -> Element {
|
||||
grid(layers).into()
|
||||
}
|
||||
|
||||
/// Spawn mode's Stream screen: the stream runs in the punktfunk-session child's own
|
||||
/// window, so the shell shows a status card in the app's card language — monogram +
|
||||
/// host header, the child's live `stats:` line as a chip row + stage lines, the
|
||||
/// in-window shortcuts, and a Disconnect that kills the child (its exit event routes
|
||||
/// the app back to the host list, same as the child's window closing). No hooks.
|
||||
pub(crate) fn session_page(ctx: &Arc<super::AppCtx>, hud: &HudSample) -> Element {
|
||||
use super::style::{avatar, card, pill, Pill};
|
||||
let host = ctx.shared.target.lock().unwrap().name.clone();
|
||||
let title = if host.is_empty() {
|
||||
"Streaming".to_string()
|
||||
} else {
|
||||
format!("Streaming to {host}")
|
||||
};
|
||||
|
||||
// Header: monogram + title + the one thing worth knowing (where the video went).
|
||||
let header: Element = grid((
|
||||
avatar(&host)
|
||||
.grid_column(0)
|
||||
.vertical_alignment(VerticalAlignment::Center),
|
||||
vstack((
|
||||
text_block(&title).font_size(18.0).semibold(),
|
||||
text_block("The stream runs in its own window \u{2014} click it to capture input.")
|
||||
.font_size(12.0)
|
||||
.foreground(ThemeRef::SecondaryText),
|
||||
))
|
||||
.spacing(2.0)
|
||||
.grid_column(1)
|
||||
.vertical_alignment(VerticalAlignment::Center)
|
||||
.margin(edges(12.0, 0.0, 0.0, 0.0)),
|
||||
))
|
||||
.columns([GridLength::Auto, GridLength::Star(1.0)])
|
||||
.into();
|
||||
|
||||
// The child prints one formatted stats line per 1 s window:
|
||||
// "<mode> · <fps> · <Mb/s> · <path> [· HDR] | e2e … | …" — the first segment becomes
|
||||
// a chip row (the decode path gets the status colour), the rest dim stage lines.
|
||||
let mut body: Vec<Element> = vec![header];
|
||||
if hud.stats_line.is_empty() {
|
||||
body.push(
|
||||
text_block("Waiting for the first stats window\u{2026}")
|
||||
.font_size(11.0)
|
||||
.foreground(ThemeRef::SecondaryText)
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
let mut segments = hud.stats_line.split(" | ");
|
||||
if let Some(first) = segments.next() {
|
||||
let chips: Vec<Element> = first
|
||||
.split(" \u{00B7} ")
|
||||
.map(str::trim)
|
||||
.filter(|c| !c.is_empty())
|
||||
.map(|c| {
|
||||
let kind = match c {
|
||||
"vulkan" | "vaapi" => Pill::Good,
|
||||
"software" => Pill::Info,
|
||||
_ => Pill::Neutral,
|
||||
};
|
||||
pill(c, kind).into()
|
||||
})
|
||||
.collect();
|
||||
body.push(hstack(chips).spacing(6.0).into());
|
||||
}
|
||||
for seg in segments {
|
||||
body.push(
|
||||
text_block(seg.trim())
|
||||
.font_size(11.0)
|
||||
.foreground(ThemeRef::SecondaryText)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
body.push(
|
||||
text_block(
|
||||
"Ctrl+Alt+Shift+Q releases input \u{00B7} Ctrl+Alt+Shift+D disconnects \u{00B7} \
|
||||
Ctrl+Alt+Shift+S stats \u{00B7} F11 fullscreen",
|
||||
)
|
||||
.font_size(11.0)
|
||||
.foreground(ThemeRef::SecondaryText)
|
||||
.margin(edges(0.0, 4.0, 0.0, 0.0))
|
||||
.into(),
|
||||
);
|
||||
body.push({
|
||||
let ctx = ctx.clone();
|
||||
button("Disconnect")
|
||||
.icon(Symbol::Cancel)
|
||||
.on_click(move || {
|
||||
// Kill the child; its exit event (the reader thread) navigates to the
|
||||
// host list, exactly like the session window closing.
|
||||
ctx.shared.session.lock().unwrap().kill();
|
||||
})
|
||||
.margin(edges(0.0, 6.0, 0.0, 0.0))
|
||||
.into()
|
||||
});
|
||||
|
||||
// One centred card, sized like the app's dialogs.
|
||||
border(card(vstack(body).spacing(12.0).width(520.0)))
|
||||
.horizontal_alignment(HorizontalAlignment::Center)
|
||||
.vertical_alignment(VerticalAlignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// How long the stream-start shortcut banner stays up (seconds of session uptime).
|
||||
const START_HINT_SECS: u32 = 6;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user