feat(client/windows): PyroWave decode + surface it in the GUI codec picker

The decoder was gated to Linux because, when it landed, the Windows client
still had its own in-process WinUI/D3D11 presenter and the PyroWave present
path there was an open question. That client has since been retired: Windows
now spawns the SAME Vulkan session presenter as Linux, and the decoder is
plain Vulkan compute on the presenter's device (no fds, no dmabuf, no D3D11
interop), so the question that gated it answered itself. pyrowave-sys already
builds on Windows too -- the Windows HOST encoder ships on it.

So this is a port by un-gating: every cfg(all(target_os = "linux", feature =
"pyrowave")) becomes any(linux, windows) -- decoder module, backend variant,
Decoder::new_pyrowave, the CODEC_PYROWAVE advertisement, the session pump's
opt-in/build/label arms, and the presenter's planar CSC pass. No new code.

Then offer it in the Windows GUI, which is what prompted this. It stays
preference-only (resolve_codec never auto-picks it) and a host or device that
can't do PyroWave just falls back down the ladder to HEVC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 16:08:59 +02:00
parent 1d587a259e
commit dab40ed98e
10 changed files with 61 additions and 39 deletions
+8 -5
View File
@@ -24,6 +24,14 @@ ffmpeg-next = "8"
opus = "0.3"
mdns-sd = "0.20"
# PyroWave decode (the opt-in wired-LAN wavelet codec, design/pyrowave-codec-plan.md
# §4.5) — pure Vulkan compute on the presenter's shared device, so it builds wherever the
# spawned Vulkan session presenter runs: Linux AND Windows (pyrowave-sys covers both; it
# is an empty stub elsewhere). `ash` only wraps the presenter's existing raw handles
# (same pinned version as pf-presenter).
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
ash = { version = "0.38", optional = true }
# Game-library fetch from the host's management API over mTLS + fingerprint pinning.
# `ureq` is small + sync (the host uses it too) and its rustls unifies with the
# workspace's (quinn's) 0.23; the pinning verifier mirrors core's private `PinVerify`.
@@ -40,11 +48,6 @@ tracing = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
pipewire = "0.9"
sdl3 = { version = "0.18", features = ["hidapi"] }
# PyroWave decode (the opt-in wired-LAN wavelet codec, design/pyrowave-codec-plan.md
# §4.5) — pure Vulkan compute on the presenter's shared device. `ash` only wraps the
# presenter's existing raw handles (same pinned version as pf-presenter).
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
ash = { version = "0.38", optional = true }
[target.'cfg(windows)'.dependencies]
wasapi = "0.23"
+6 -3
View File
@@ -41,11 +41,14 @@ mod video_software;
mod video_vaapi;
#[cfg(any(target_os = "linux", windows))]
mod video_vulkan;
// PyroWave decode — Linux + `pyrowave` feature only (plan §4.5; the Windows client's
// present-path decision and the Apple Metal port are their own phases).
// PyroWave decode — Linux + Windows (plan §4.5; the Apple Metal port is its own phase).
// Windows joined once its client moved to the SAME spawned Vulkan session presenter as
// Linux's: the decoder is plain Vulkan compute on the presenter's device (no fds, no
// dmabuf, no D3D11 interop), so the old "Windows present-path decision" that gated it
// resolved itself — the present path is now literally the same code.
#[cfg(windows)]
pub mod video_d3d11;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
pub mod video_pyrowave;
pub mod wol;
+8 -5
View File
@@ -227,7 +227,7 @@ fn pump(
// the plan-§3 contract: the host only ever picks PyroWave when the client names it.
#[allow(unused_mut)]
let mut preferred = params.preferred_codec;
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
if std::env::var("PUNKTFUNK_PREFER_PYROWAVE").as_deref() == Ok("1") {
if params.vulkan.as_ref().is_some_and(|v| v.pyrowave_decode) {
preferred = punktfunk_core::quic::CODEC_PYROWAVE;
@@ -296,7 +296,7 @@ fn pump(
// A negotiated PyroWave session decodes on the presenter's device, no FFmpeg —
// reachable only through the explicit preference above (resolve_codec never
// auto-picks the bit), so failing loudly here is failing an opted-in experiment.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
let built = if connector.codec == punktfunk_core::quic::CODEC_PYROWAVE {
let mode = connector.mode();
// The wavelet bitstream has no VUI: the negotiated Welcome colour signalling IS
@@ -325,7 +325,7 @@ fn pump(
} else {
Decoder::new(codec_id, &params.decoder, params.vulkan.as_ref())
};
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
#[cfg(not(all(any(target_os = "linux", windows), feature = "pyrowave")))]
let built = Decoder::new(codec_id, &params.decoder, params.vulkan.as_ref());
let mut decoder = match built {
Ok(d) => d,
@@ -528,7 +528,7 @@ fn pump(
DecodedImage::VkFrame(_) => "vulkan",
#[cfg(windows)]
DecodedImage::D3d11(_) => "d3d11va",
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
DecodedImage::PyroWave(_) => "pyrowave",
};
if total_frames == 1 {
@@ -539,7 +539,10 @@ fn pump(
DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"),
#[cfg(windows)]
DecodedImage::D3d11(d) => (d.width, d.height, "d3d11va"),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(
any(target_os = "linux", windows),
feature = "pyrowave"
))]
DecodedImage::PyroWave(f) => (f.width, f.height, "pyrowave"),
};
tracing::info!(width = w, height = h, path, "first frame decoded");
+20 -10
View File
@@ -77,7 +77,7 @@ pub enum DecodedImage {
/// PyroWave planar output: three R8 plane views on the presenter's own device,
/// decode already fence-complete, GENERAL layout — the presenter's planar CSC
/// samples them directly (BT.709 limited, the codec's fixed colour contract).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
PyroWave(crate::video_pyrowave::PyroWavePlanarFrame),
}
@@ -155,7 +155,7 @@ impl DecodedImage {
DecodedImage::VkFrame(f) => f.keyframe,
#[cfg(windows)]
DecodedImage::D3d11(f) => f.keyframe,
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
DecodedImage::PyroWave(f) => f.keyframe,
}
}
@@ -171,7 +171,7 @@ impl DecodedImage {
DecodedImage::VkFrame(f) => (f.width, f.height),
#[cfg(windows)]
DecodedImage::D3d11(f) => (f.width, f.height),
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
DecodedImage::PyroWave(f) => (f.width, f.height),
}
}
@@ -238,9 +238,10 @@ enum Backend {
#[cfg(windows)]
D3d11va(crate::video_d3d11::D3d11vaDecoder),
/// PyroWave (wired-LAN wavelet codec): pyrowave compute on the presenter's device,
/// no FFmpeg involvement. No demotion rung — there is no other decoder for it.
/// no FFmpeg involvement (Linux + Windows — same Vulkan presenter on both). No demotion
/// rung — there is no other decoder for it.
/// Boxed: the decoder (pinned create-info hold + plane ring) dwarfs the other variants.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
PyroWave(Box<crate::video_pyrowave::PyroWaveDecoder>),
Software(SoftwareDecoder),
}
@@ -322,11 +323,11 @@ pub fn decodable_codecs() -> u8 {
/// under its explicit opt-in.
pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 {
let bits = decodable_codecs();
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
if vk.map(|v| v.pyrowave_decode).unwrap_or(false) {
return bits | punktfunk_core::quic::CODEC_PYROWAVE;
}
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
#[cfg(not(all(any(target_os = "linux", windows), feature = "pyrowave")))]
let _ = vk;
bits
}
@@ -572,7 +573,7 @@ impl Decoder {
/// Open a PyroWave decoder for a `CODEC_PYROWAVE` session (plan §4.5): pyrowave
/// compute on the presenter's device, no FFmpeg. `codec_id` is irrelevant (kept as
/// HEVC so an — impossible — demotion path stays well-formed).
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
pub fn new_pyrowave(
vk: &VulkanDecodeDevice,
width: u32,
@@ -596,6 +597,15 @@ impl Decoder {
vaapi_fails: 0,
first_fail: None,
want_keyframe: false,
// A PyroWave session never demotes (nothing else decodes it — a failure
// renegotiates the codec instead), so the D3D11VA rebuild facts are unused
// here; keep them well-formed rather than plumbing them in for nothing.
#[cfg(windows)]
d3d11_import: false,
#[cfg(windows)]
adapter_luid: None,
#[cfg(windows)]
d3d11_hdr10: false,
})
}
@@ -639,7 +649,7 @@ impl Decoder {
au: &[u8],
// Only the PyroWave backend reads the flags; without that feature the param is unused.
#[cfg_attr(
not(all(target_os = "linux", feature = "pyrowave")),
not(all(any(target_os = "linux", windows), feature = "pyrowave")),
allow(unused_variables)
)]
user_flags: u32,
@@ -657,7 +667,7 @@ impl Decoder {
// No demote ladder below PyroWave (nothing else decodes it): propagate the
// error; the pump surfaces it and the session falls back to HEVC by
// renegotiation (plan §4.6), not by decoder swap.
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
Backend::PyroWave(p) => {
let aligned = user_flags & punktfunk_core::packet::USER_FLAG_CHUNK_ALIGNED != 0;
return Ok(p