diff --git a/clients/windows/src/app/settings.rs b/clients/windows/src/app/settings.rs index a83210e8..63c8283d 100644 --- a/clients/windows/src/app/settings.rs +++ b/clients/windows/src/app/settings.rs @@ -51,6 +51,9 @@ const CODECS: &[(&str, &str)] = &[ ("hevc", "HEVC (H.265)"), ("h264", "H.264 (AVC)"), ("av1", "AV1"), + // Preference-only by design: `resolve_codec` never auto-picks PyroWave, and asking for + // it on a host or device that can't do it simply falls back down the ladder to HEVC. + ("pyrowave", "PyroWave (wired LAN)"), ]; /// Virtual-pad presets: `(stored value, display label)` — the pad the HOST creates. Same set the /// GTK client offers; "Automatic" resolves from the physical controller at connect. @@ -278,7 +281,7 @@ pub(crate) fn settings_page( s.codec = CODECS[i].0.to_string(); }) .tooltip( - "A soft preference \u{2014} the host falls back to the best codec both sides support.", + "A soft preference \u{2014} the host falls back to the best codec both sides support. PyroWave is the low-latency wavelet codec for a WIRED link: it trades bitrate (hundreds of Mb/s) for near-zero decode time, so it needs gigabit Ethernet.", ); // Free-form Mb/s (0 = host default) instead of presets, so a speed-test recommendation // round-trips exactly. diff --git a/crates/pf-client-core/Cargo.toml b/crates/pf-client-core/Cargo.toml index 6a759060..8f43a0ac 100644 --- a/crates/pf-client-core/Cargo.toml +++ b/crates/pf-client-core/Cargo.toml @@ -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" diff --git a/crates/pf-client-core/src/lib.rs b/crates/pf-client-core/src/lib.rs index 53c3f0a9..32f7f511 100644 --- a/crates/pf-client-core/src/lib.rs +++ b/crates/pf-client-core/src/lib.rs @@ -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; diff --git a/crates/pf-client-core/src/session.rs b/crates/pf-client-core/src/session.rs index 99cd8151..d91bcd8a 100644 --- a/crates/pf-client-core/src/session.rs +++ b/crates/pf-client-core/src/session.rs @@ -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, ¶ms.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, ¶ms.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"); diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index d808462b..5b808a0c 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -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), 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 diff --git a/crates/pf-presenter/src/run.rs b/crates/pf-presenter/src/run.rs index 22c957c2..582955b5 100644 --- a/crates/pf-presenter/src/run.rs +++ b/crates/pf-presenter/src/run.rs @@ -194,7 +194,7 @@ struct StreamState { /// PyroWave present has no demote rung (nothing else decodes the codec), so a /// persistent non-device-lost present failure would warn on every frame. Latch it: /// warn on the first failure of a streak, then stay quiet until a present succeeds. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pyro_present_warned: bool, hw_fails: u32, /// The OSD's text (multi-line; rebuilt each Stats window and on a live tier cycle). @@ -267,7 +267,7 @@ impl StreamState { win_start: Instant::now(), presented: PresentedWindow::default(), dmabuf_demoted: false, - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pyro_present_warned: false, hw_fails: 0, osd_text: String::new(), @@ -1007,7 +1007,7 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result // PyroWave planar frames: already on the presenter's device and // fence-complete — a present failure has no demote rung (nothing // else decodes the codec); only device loss ends the session. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] DecodedImage::PyroWave(f) => { // The wavelet stream carries the negotiated ColorInfo (no VUI): an // HDR (PQ) pyrowave session presents through the HDR10 path exactly diff --git a/crates/pf-presenter/src/vk/mod.rs b/crates/pf-presenter/src/vk/mod.rs index 546b5eb7..4c3c9807 100644 --- a/crates/pf-presenter/src/vk/mod.rs +++ b/crates/pf-presenter/src/vk/mod.rs @@ -47,7 +47,7 @@ pub enum FrameInput<'a> { D3d11(pf_client_core::video::D3d11Frame), /// PyroWave planar output — three R8 plane views already on THIS device, decode /// fence-complete, GENERAL layout (`pf_client_core::video_pyrowave`). - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] PyroWave(pf_client_core::video_pyrowave::PyroWavePlanarFrame), } @@ -136,7 +136,7 @@ pub struct Presenter { csc: CscPass, /// The planar (3-plane) CSC variant for PyroWave frames; built only when the device /// passed the pyrowave probe. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] csc_planar: Option, /// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it. video_export: Option, @@ -304,7 +304,7 @@ impl Drop for Presenter { #[cfg(target_os = "linux")] self.hw.take(); self.csc.destroy(&self.device); - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let Some(p) = &self.csc_planar { p.destroy(&self.device); } diff --git a/crates/pf-presenter/src/vk/present.rs b/crates/pf-presenter/src/vk/present.rs index 0a1f26fd..3c451cda 100644 --- a/crates/pf-presenter/src/vk/present.rs +++ b/crates/pf-presenter/src/vk/present.rs @@ -38,7 +38,7 @@ impl Presenter { FrameInput::VkFrame(v) => Some(v.color.is_pq()), #[cfg(windows)] FrameInput::D3d11(d) => Some(d.color.is_pq()), - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => Some(f.color.is_pq()), }; if let Some(pq) = frame_pq { @@ -68,7 +68,7 @@ impl Presenter { #[cfg(windows)] let mut win_frame: Option = None; let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None; - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] let mut pyro_frame: Option = None; let cpu_frame = match input { FrameInput::Redraw => None, @@ -96,7 +96,7 @@ impl Presenter { vk_frame = Some((v, views)); None } - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => { pyro_frame = Some(f); None @@ -155,7 +155,7 @@ impl Presenter { } self.csc.bind_planes(&self.device, views[0], views[1]); } - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let Some(f) = &pyro_frame { if self .video @@ -317,7 +317,7 @@ impl Presenter { // PyroWave frame: the planes are already on THIS device, decode // fence-complete and barriered to fragment sampling (GENERAL) by the // decoder — no acquire needed, just the planar CSC pass. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let (Some(f), Some(v)) = (&pyro_frame, &self.video) { let extent = vk::Extent2D { width: v.width, @@ -695,7 +695,7 @@ impl Presenter { } /// [`record_csc`] over the planar (PyroWave) pass — always 8-bit, no MSB packing. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] unsafe fn record_csc_planar( &self, framebuffer: vk::Framebuffer, diff --git a/crates/pf-presenter/src/vk/reconfig.rs b/crates/pf-presenter/src/vk/reconfig.rs index ac3e37ea..6fe96ed3 100644 --- a/crates/pf-presenter/src/vk/reconfig.rs +++ b/crates/pf-presenter/src/vk/reconfig.rs @@ -207,7 +207,7 @@ impl Presenter { // The planar (PyroWave) pass renders to the same intermediate — rebuild it at the // new format too (an HDR pyrowave session needs the 10-bit intermediate exactly // like the H.26x path; 8-bit PQ bands visibly). - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let Some(p) = self.csc_planar.take() { p.destroy(&self.device); self.csc_planar = Some(CscPass::new_planar(&self.device, self.video_format)?); diff --git a/crates/pf-presenter/src/vk/setup.rs b/crates/pf-presenter/src/vk/setup.rs index 57983ebb..8b2081a0 100644 --- a/crates/pf-presenter/src/vk/setup.rs +++ b/crates/pf-presenter/src/vk/setup.rs @@ -318,7 +318,7 @@ impl Presenter { let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?; // Starts SDR like `csc`; an HDR (PQ) pyrowave session rebuilds it at the 10-bit // intermediate via `set_hdr_mode`, exactly like the H.26x pass. - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] let csc_planar = if pyrowave_ok { Some(CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?) } else { @@ -463,7 +463,7 @@ impl Presenter { #[cfg(windows)] hw_win, csc, - #[cfg(all(target_os = "linux", feature = "pyrowave"))] + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] csc_planar, video_export, overlay_pipe,