diff --git a/CLAUDE.md b/CLAUDE.md index 4bfc3c3..5dae82a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,39 +73,43 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc `send_rich_input`. **Client-negotiated virtual pad type**: the Hello carries a gamepad preference byte (same trailing-byte back-compat pattern as the compositor), the Welcome echoes the resolved backend — precedence: explicit client choice > `PUNKTFUNK_GAMEPAD` - env > uinput Xbox 360. Backends: **Xbox 360** (uinput / ViGEm), **Xbox One/Series** (the same + env > uinput Xbox 360. Backends: **Xbox 360** (uinput on Linux / the pf-xusb UMDF driver on + Windows), **Xbox One/Series** (the same XInput backend with the One/Series USB identity for matching glyphs — no extra game-visible capability; impulse-trigger rumble is unreachable through a virtual pad), and the UHID `hid-playstation` pads — **DualSense** (adaptive triggers, lightbar, touchpad, motion) and **DualShock 4** (lightbar, touchpad, motion, rumble; DualSense minus adaptive triggers / player LEDs / mute). DualSense and DualShock 4 each have a Linux (UHID `hid-playstation`) **and a Windows - (UMDF minidriver)** backend — `inject/dualsense_windows.rs` + `inject/dualshock4_windows.rs`, one + (UMDF minidriver)** backend — `inject/windows/dualsense_windows.rs` + `inject/windows/dualshock4_windows.rs`, one driver serving either identity per a `device_type` byte the host stamps into shared memory (the DS4 reuses the same SwDeviceCreate game-detection identity fix as the DualSense). One/Series stays Linux-only and folds into Xbox 360 off it. Clients auto-resolve the type from the physical controller (DS5→DualSense, DS4→DualShock 4, Xbox One→Xbox One). **Windows uses ZERO external gamepad dependencies — ViGEmBus is gone.** Xbox 360 (XInput) runs on a UMDF2 **XUSB companion** driver - (`packaging/windows/xusb-driver/`, `inject/gamepad_windows.rs`) that registers `GUID_DEVINTERFACE_XUSB` + (`packaging/windows/drivers/pf-xusb/`, `inject/windows/gamepad_windows.rs`) that registers `GUID_DEVINTERFACE_XUSB` and answers the buffered XInput IOCTLs from a shared section, so classic `XInputGetState`/`SetState` work with no kernel bus driver (validated live: slot connected, state + rumble round-trip; Xbox One - folds to this 360 path). All three UMDF drivers (DualSense/DS4 + XUSB) are bundled + pnputil-installed - by the Inno Setup installer (`packaging/windows/gamepad-drivers/` + `install-gamepad-drivers.ps1`). + folds to this 360 path). All three UMDF drivers (DualSense/DS4 + XUSB) are built from source in CI + (`packaging/windows/drivers/`) and installed by the Inno Setup installer via + `punktfunk-host.exe driver install --gamepad`. **Multi-pad ready**: the host stamps each pad's index into the device Location (`pszDeviceLocation`), the driver reads it (`WdfDeviceAllocAndQueryProperty`) to map its own `*-shm-`, and `UmdfHostProcessSharing=ProcessSharingDisabled` gives each pad its own host (per-pad statics) — validated live with 2 distinct XInput slots + 2 DualSense pads. (Client-side multi-pad forwarding is the remaining piece.) - **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** - virtual display per session (`vdisplay/sudovda.rs`), GPU encode (NVENC `--features nvenc`; AMD/Intel - `--features amf-qsv`), SendInput + **ViGEm** gamepads (`inject/gamepad_windows.rs`), WASAPI loopback - + virtual mic (`audio/wasapi_*`). Ships as a **signed Inno Setup installer** that registers a - `LocalSystem` SCM service launching into the interactive session for secure-desktop (UAC/lock-screen) - capture (`service.rs`), bundles the SudoVDA driver + the FFmpeg DLLs, and is published by + behind the same traits as Linux — **IDD-push capture** straight into the in-house all-Rust IddCx + **pf-vdisplay** virtual display (`capture/windows/idd_push.rs`, `vdisplay/windows/pf_vdisplay.rs`; + DXGI Desktop Duplication / WGC as fallbacks, `capture/windows/dxgi.rs`), GPU encode (NVENC + `--features nvenc`; AMD/Intel `--features amf-qsv`), SendInput + the in-house UMDF gamepad drivers + (`inject/windows/`), WASAPI loopback + virtual mic (`audio/windows/wasapi_*`). Ships as a **signed + Inno Setup installer** that registers a `LocalSystem` SCM service launching into the interactive + session for secure-desktop (UAC/lock-screen) capture (`windows/service.rs`), bundles the + pf-vdisplay driver + the FFmpeg DLLs (+ VB-CABLE for the virtual mic), and is published by `windows-host.yml`. **Encoder is GPU-aware** (`encode.rs` `open_video` + `windows_resolved_backend`): `PUNKTFUNK_ENCODER=auto` (the host.env default) detects the DXGI adapter vendor → **NVENC** (NVIDIA, - direct SDK, `encode/nvenc.rs`), **AMF** (AMD) / **QSV** (Intel) via libavcodec - (`encode/ffmpeg_win.rs`, the Windows analogue of the Linux VAAPI backend — `WinVendor{Amf,Qsv}`, + direct SDK, `encode/windows/nvenc.rs`), **AMF** (AMD) / **QSV** (Intel) via libavcodec + (`encode/windows/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 @@ -311,18 +315,22 @@ land on an already-provisioned box instead of the one that actually needed it. crates/punktfunk-core/ protocol · FEC · crypto · quic (punktfunk/1 control plane, feature-gated) crates/punktfunk-host/ gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps - 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) - 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 · stats_recorder.rs + vdisplay/linux/{kwin,gamescope,mutter,wlroots}.rs per-compositor client-sized virtual outputs + vdisplay/windows/{pf_vdisplay,manager,identity}.rs all-Rust IddCx virtual display (pf-vdisplay) + linux/zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan) + inject/linux/{libei,wlr,gamepad,dualsense,dualshock4,steam_*}.rs Linux input (uinput xpad · UHID pads · virtual Deck) + inject/windows/{sendinput,gamepad_windows,dualsense_windows,dualshock4_windows}.rs Windows input (UMDF shared-mem pads) + encode/linux/{mod,vaapi}.rs · encode/windows/{nvenc,ffmpeg_win}.rs · encode/sw.rs per-GPU encoders (NVENC/CUDA · VAAPI · AMF/QSV) + GPU-less openh264 + capture/{linux/,windows/{dxgi,idd_push}}.rs · audio/{linux/,windows/wasapi_*}.rs + windows/{service,install,interactive}.rs SCM service + in-binary driver/web install + capture.rs · encode.rs · audio.rs · spike.rs · punktfunk1.rs · mgmt.rs · native_pairing.rs · stats_recorder.rs · library.rs clients/probe/ punktfunk/1 reference/probe client (headless test/measurement tool) clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3) clients/windows/ native Windows client (WinUI 3 via windows-reactor · D3D11 · WASAPI · SDL3) 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/decky/ Steam Deck Decky plugin -crates/punktfunk-host/src/{capture/dxgi,vdisplay/sudovda,encode/ffmpeg_win,inject/gamepad_windows,audio/wasapi_*,service}.rs Windows host backends +packaging/windows/drivers/{pf-vdisplay,pf-dualsense,pf-xusb}/ in-house UMDF drivers (built from source in CI) web/ TanStack web console over the mgmt API (status · devices · pairing · performance graphs) packaging/ apt(deb) · RPM/COPR · Arch/sysext · Flatpak · Bazzite bootc · Windows host installer (per-dir READMEs) tools/{loss-harness,latency-probe}/ measurement (plan §10) diff --git a/README.md b/README.md index d893b97..981eb37 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ protocol, FEC, and crypto, linked into the host and every client over a stable C a screen — tight, push-based integration that's unusual for a Windows streaming host. - **Low latency, GPU end to end.** Frames go straight from the compositor to the NVENC encoder with zero CPU copies (dmabuf → CUDA/Vulkan → NVENC), over a transport tuned for responsiveness rather - than throughput. Stable 240 fps at 5120×1440; sub-millisecond capture-to-reassembly on a LAN. + than throughput. Stable 240 fps at 5120×1440; sub-millisecond capture-to-reassembly on-box, + ~1.3 ms cross-machine on a LAN. (AMD/Intel encode via VAAPI, and a GPU-less software H.264 + encoder exists as a fallback.) - **Works with what you already have.** Any Moonlight/Artemis client connects over GameStream — and native apps for macOS, Linux, Windows, and Android use the lower-latency `punktfunk/1` protocol. - **Secure by default.** Hosts require a one-time SPAKE2 **PIN pairing**; after that, devices @@ -47,7 +49,7 @@ protocol, FEC, and crypto, linked into the host and every client over a stable C | **Core** — `punktfunk-core` + C ABI (protocol · FEC · crypto · QUIC) | ✅ Complete & hardened | | **GameStream host** → stock Moonlight | ✅ Live end-to-end: pairing, RTSP, audio, per-client virtual output at native resolution, GPU zero-copy NVENC, gamepads | | **Native protocol** — `punktfunk/1` | ✅ Validated live: QUIC control + GF(2¹⁶) FEC/AES-GCM data plane, PIN pairing, mDNS discovery, mid-stream mode renegotiation | -| **Windows host** (x64) | 🟡 Implemented & shipping as a signed installer: DXGI/WGC capture · its own all-Rust IddCx **virtual display** (secure-desktop capable) · GPU encode (NVENC on NVIDIA, AMF/QSV on AMD/Intel) · WASAPI audio · bundled virtual-gamepad drivers (no ViGEmBus) · HDR incl. Vulkan-game HDR. NVIDIA live-validated; AMD/Intel CI-green | +| **Windows host** (x64) | 🟡 Implemented & shipping as a signed installer: DXGI/WGC capture · its own all-Rust IddCx **virtual display** (secure-desktop capable) · GPU encode (NVENC on NVIDIA, AMF/QSV on AMD/Intel, software H.264 without a GPU) · WASAPI audio · bundled virtual-gamepad drivers (no ViGEmBus) · HDR incl. Vulkan-game HDR. NVIDIA live-validated; AMD/Intel CI-green | | **macOS / iOS / tvOS client** (`clients/apple`) | ✅ Streaming live: VideoToolbox decode, controllers incl. DualSense, discovery, pairing, speed test | | **Linux client** (`clients/linux`, GTK4) | ✅ Streaming live: FFmpeg + VAAPI zero-copy decode, PipeWire audio, SDL3 controllers; ships as Flatpak/apt/rpm/Arch | | **Android client** (`clients/android`, phone + TV) | ✅ Streaming live: AMediaCodec decode + HDR10, Oboe audio, controllers, discovery, pairing | diff --git a/clients/apple/README.md b/clients/apple/README.md index f7371dd..65d697e 100644 --- a/clients/apple/README.md +++ b/clients/apple/README.md @@ -107,7 +107,7 @@ PUNKTFUNK_AUTOCONNECT= PUNKTFUNK_MODE=1280x720x60 swift run PunktfunkCli - **App Store screenshots** are automated — `tools/screenshots.sh all` renders the real UI at the required pixel sizes via a DEBUG-only shot mode; the `apple` CI workflow captures the iOS sizes on every main push. See the script header for details. -- Deeper design notes live in `docs-site/content/docs/apple-stage2-presenter.md`. +- Deeper design notes live in [`design/apple-stage2-presenter.md`](../../design/apple-stage2-presenter.md). ## Related diff --git a/clients/linux/src/app.rs b/clients/linux/src/app.rs index 6912fe8..623a582 100644 --- a/clients/linux/src/app.rs +++ b/clients/linux/src/app.rs @@ -529,9 +529,11 @@ fn speed_test(app: Rc, req: ConnectRequest) { }, CompositorPref::Auto, GamepadPref::Auto, - 0, // bitrate_kbps (host default) - 0, // video_caps: the Linux client has no 10-bit/HDR present path yet - 2, // audio_channels: speed-test probe, stereo + 0, // bitrate_kbps (host default) + 0, // video_caps: the Linux client has no 10-bit/HDR present path yet + 2, // audio_channels: speed-test probe, stereo + crate::video::decodable_codecs(), // codecs (unused by the probe, but honest) + 0, // preferred_codec: no preference for a speed-test probe None, // launch: speed-test probe connect, no game pin, Some(identity), @@ -689,6 +691,7 @@ fn start_session_with(app: Rc, req: ConnectRequest, pin: Option<[u8; 32]>, bitrate_kbps: s.bitrate_kbps, mic_enabled: s.mic_enabled, audio_channels: s.audio_channels, + preferred_codec: s.preferred_codec(), pin, identity: app.identity.clone(), connect_timeout: opts.connect_timeout, diff --git a/clients/linux/src/session.rs b/clients/linux/src/session.rs index 10f1056..c2c6276 100644 --- a/clients/linux/src/session.rs +++ b/clients/linux/src/session.rs @@ -22,6 +22,9 @@ pub struct SessionParams { pub bitrate_kbps: u32, /// Requested audio channel count (2/6/8); the host echoes the resolved value. pub audio_channels: u8, + /// The user's preferred video codec (a `quic::CODEC_*` bit, `0` = auto). Soft — the host honors + /// it when it can emit it, else falls back; the resolved codec drives the decoder. + pub preferred_codec: u8, /// Stream the default microphone to the host's virtual mic source. pub mic_enabled: bool, /// Pinned host fingerprint; `None` = trust on first use (caller persists the observed one). @@ -141,7 +144,9 @@ fn pump( params.bitrate_kbps, 0, // video_caps: the Linux client has no 10-bit/HDR present path yet params.audio_channels, - None, // launch: the Linux client has no library picker yet + crate::video::decodable_codecs(), // codecs FFmpeg can decode (HEVC/H.264/AV1) + params.preferred_codec, // the user's soft codec preference (0 = auto) + None, // launch: the Linux client has no library picker yet params.pin, Some(params.identity), params.connect_timeout, @@ -170,7 +175,14 @@ fn pump( fingerprint: connector.host_fingerprint, }); - let mut decoder = match Decoder::new() { + // Build the decoder for the codec the host resolved (never assume HEVC). + let codec_id = crate::video::ffmpeg_codec_id(connector.codec); + tracing::info!( + ?codec_id, + welcome_codec = connector.codec, + "negotiated video codec" + ); + let mut decoder = match Decoder::new(codec_id) { Ok(d) => d, Err(e) => { let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}")))); diff --git a/clients/linux/src/trust.rs b/clients/linux/src/trust.rs index f95cbac..eb676bc 100644 --- a/clients/linux/src/trust.rs +++ b/clients/linux/src/trust.rs @@ -135,6 +135,26 @@ pub struct Settings { /// Requested audio channel count: 2 (stereo), 6 (5.1) or 8 (7.1). The host clamps to what it /// can capture; the resolved count drives the decoder + playback layout. pub audio_channels: u8, + /// Preferred video codec: `"auto"` (host decides), `"hevc"`, `"h264"`, or `"av1"`. A soft + /// preference — the host honors it when it can emit it, else falls back to the best shared codec. + #[serde(default = "default_codec")] + pub codec: String, +} + +fn default_codec() -> String { + "auto".into() +} + +impl Settings { + /// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto). + pub fn preferred_codec(&self) -> u8 { + match self.codec.as_str() { + "h264" | "avc" => punktfunk_core::quic::CODEC_H264, + "hevc" | "h265" => punktfunk_core::quic::CODEC_HEVC, + "av1" => punktfunk_core::quic::CODEC_AV1, + _ => 0, + } + } } impl Default for Settings { @@ -149,6 +169,7 @@ impl Default for Settings { inhibit_shortcuts: true, mic_enabled: false, audio_channels: 2, + codec: "auto".into(), } } } diff --git a/clients/linux/src/ui_settings.rs b/clients/linux/src/ui_settings.rs index 71d3f8d..46af284 100644 --- a/clients/linux/src/ui_settings.rs +++ b/clients/linux/src/ui_settings.rs @@ -18,6 +18,9 @@ const RESOLUTIONS: &[(u32, u32)] = &[ const REFRESH: &[u32] = &[0, 30, 60, 90, 120, 144, 165, 240]; const GAMEPADS: &[&str] = &["auto", "xbox360", "dualsense", "xboxone", "dualshock4"]; const COMPOSITORS: &[&str] = &["auto", "kwin", "wlroots", "mutter", "gamescope"]; +/// Codec setting values (persisted) paired with their display labels below. +const CODECS: &[&str] = &["auto", "hevc", "h264", "av1"]; +const CODEC_LABELS: &[&str] = &["Automatic", "HEVC (H.265)", "H.264 (AVC)", "AV1"]; /// punktfunk's own license (MIT OR Apache-2.0), shown on the About dialog's Legal page. const APP_LICENSE: &str = concat!( @@ -193,6 +196,12 @@ pub fn show( ])) .build(); audio.add(&surround_row); + let codec_row = adw::ComboRow::builder() + .title("Video codec") + .subtitle("Preferred codec — the host falls back if it can't encode this one") + .model(>k::StringList::new(CODEC_LABELS)) + .build(); + stream.add(&codec_row); let mic_row = adw::SwitchRow::builder() .title("Stream microphone") .subtitle("Send the default input device to the host's virtual microphone") @@ -242,6 +251,8 @@ pub fn show( 8 => 2, _ => 0, }); + let codec_i = CODECS.iter().position(|&c| c == s.codec).unwrap_or(0); + codec_row.set_selected(codec_i as u32); } let dialog = adw::PreferencesDialog::new(); @@ -263,6 +274,7 @@ pub fn show( 2 => 8, _ => 2, }; + s.codec = CODECS[(codec_row.selected() as usize).min(CODECS.len() - 1)].to_string(); s.save(); }); dialog.present(Some(parent)); diff --git a/clients/linux/src/video.rs b/clients/linux/src/video.rs index 2f79e64..7f449f5 100644 --- a/clients/linux/src/video.rs +++ b/clients/linux/src/video.rs @@ -76,18 +76,48 @@ enum Backend { pub struct Decoder { backend: Backend, + /// The negotiated codec (from the host's Welcome), so a mid-session VAAPI→software demotion + /// rebuilds the software decoder for the SAME codec. + codec_id: ffmpeg::codec::Id, +} + +/// Map a negotiated `quic` codec bit to the FFmpeg decoder id the client opens. +pub fn ffmpeg_codec_id(wire: u8) -> ffmpeg::codec::Id { + match wire { + punktfunk_core::quic::CODEC_H264 => ffmpeg::codec::Id::H264, + punktfunk_core::quic::CODEC_AV1 => ffmpeg::codec::Id::AV1, + _ => ffmpeg::codec::Id::HEVC, + } +} + +/// The `quic` codec bitfield this client can decode — whatever FFmpeg has a decoder for (HEVC/H.264 +/// always; AV1 when built in). Advertised to the host so it never emits a codec we can't decode. +pub fn decodable_codecs() -> u8 { + let _ = ffmpeg::init(); + let mut bits = 0u8; + for (id, bit) in [ + (ffmpeg::codec::Id::HEVC, punktfunk_core::quic::CODEC_HEVC), + (ffmpeg::codec::Id::H264, punktfunk_core::quic::CODEC_H264), + (ffmpeg::codec::Id::AV1, punktfunk_core::quic::CODEC_AV1), + ] { + if ffmpeg::decoder::find(id).is_some() { + bits |= bit; + } + } + bits } impl Decoder { - pub fn new() -> Result { + pub fn new(codec_id: ffmpeg::codec::Id) -> Result { ffmpeg::init().context("ffmpeg init")?; let choice = std::env::var("PUNKTFUNK_DECODER").unwrap_or_default(); if choice != "software" { - match VaapiDecoder::new() { + match VaapiDecoder::new(codec_id) { Ok(v) => { - tracing::info!("VAAPI hardware decode active (zero-copy dmabuf)"); + tracing::info!(?codec_id, "VAAPI hardware decode active (zero-copy dmabuf)"); return Ok(Decoder { backend: Backend::Vaapi(v), + codec_id, }); } Err(e) => { @@ -99,7 +129,8 @@ impl Decoder { } } Ok(Decoder { - backend: Backend::Software(SoftwareDecoder::new()?), + backend: Backend::Software(SoftwareDecoder::new(codec_id)?), + codec_id, }) } @@ -113,7 +144,7 @@ impl Decoder { Ok(f) => Ok(f.map(DecodedFrame::Dmabuf)), Err(e) => { tracing::warn!(error = %e, "VAAPI decode failed — falling back to software"); - self.backend = Backend::Software(SoftwareDecoder::new()?); + self.backend = Backend::Software(SoftwareDecoder::new(self.codec_id)?); Ok(None) } }, @@ -131,9 +162,9 @@ struct SoftwareDecoder { } impl SoftwareDecoder { - fn new() -> Result { - let codec = - ffmpeg::decoder::find(ffmpeg::codec::Id::HEVC).ok_or(anyhow!("no HEVC decoder"))?; + fn new(codec_id: ffmpeg::codec::Id) -> Result { + let codec = ffmpeg::decoder::find(codec_id) + .ok_or_else(|| anyhow!("no {codec_id:?} decoder in libavcodec"))?; let mut ctx = ffmpeg::codec::Context::new_with_codec(codec); unsafe { let raw = ctx.as_mut_ptr(); @@ -142,7 +173,7 @@ impl SoftwareDecoder { (*raw).thread_type = ffmpeg::ffi::FF_THREAD_SLICE; (*raw).thread_count = 0; // auto } - let decoder = ctx.decoder().video().context("open HEVC decoder")?; + let decoder = ctx.decoder().video().context("open video decoder")?; Ok(SoftwareDecoder { decoder, sws: None }) } @@ -240,7 +271,7 @@ struct VaapiDecoder { unsafe impl Send for VaapiDecoder {} impl VaapiDecoder { - fn new() -> Result { + fn new(codec_id: ffmpeg::codec::Id) -> Result { use ffmpeg::ffi; unsafe { let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut(); @@ -254,10 +285,11 @@ impl VaapiDecoder { if r < 0 { bail!("no VAAPI device ({})", ffmpeg::Error::from(r)); } - let codec = ffi::avcodec_find_decoder(ffi::AVCodecID::AV_CODEC_ID_HEVC); + // The negotiated codec's decoder id (av_codec_id maps 1:1 from ffmpeg::codec::Id). + let codec = ffi::avcodec_find_decoder(codec_id.into()); if codec.is_null() { ffi::av_buffer_unref(&mut hw_device); - bail!("no HEVC decoder"); + bail!("no {codec_id:?} decoder"); } let ctx = ffi::avcodec_alloc_context3(codec); (*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device); diff --git a/clients/probe/README.md b/clients/probe/README.md index 611ade5..8cc4dde 100644 --- a/clients/probe/README.md +++ b/clients/probe/README.md @@ -12,8 +12,10 @@ example of driving the protocol end to end: QUIC control plane, UDP data plane, ## What it does -- **Receives a real stream**, writes a playable `.h265`, and reports per-frame - **capture→…→reassembled latency** percentiles (the host stamps each frame with its capture clock). +- **Receives a real stream**, writes a playable elementary stream (`.h265`/`.h264`/`.av1` — the + extension tracks the **negotiated codec**; the probe advertises all three and the host picks), and + reports per-frame **capture→…→reassembled latency** percentiles (the host stamps each frame with + its capture clock). - **Verification mode** against a synthetic host — byte-checks deterministic test frames. - **Exercises every plane** with scripted test traffic: `--input-test` (mouse/keyboard), `--mic-test` (a 440 Hz Opus tone up to the host mic), @@ -25,7 +27,10 @@ example of driving the protocol end to end: QUIC control plane, UDP data plane, - **Discovery** — `--discover [secs]` browses the LAN for `_punktfunk._udp` hosts and prints each (name, addr:port, pairing requirement, cert fingerprint), then exits. - **Negotiation knobs** — `--mode WxHxFPS`, `--remode` (mid-stream mode change), `--bitrate`, - `--audio-channels` (stereo / 5.1 / 7.1), `--compositor`, `--gamepad`, `--launch`, `--speed-test`. + `--codec auto|h264|hevc|av1` (preference; the host resolves), `--audio-channels` + (stereo / 5.1 / 7.1), `--compositor`, `--gamepad`, `--launch`, `--speed-test`. + Env: `PUNKTFUNK_CLIENT_10BIT=1` / `PUNKTFUNK_CLIENT_444=1` advertise the 10-bit / 4:4:4 client + caps (for testing a host's `PUNKTFUNK_10BIT`/`PUNKTFUNK_444`). ## Usage diff --git a/clients/probe/src/main.rs b/clients/probe/src/main.rs index 5faaa4f..58cd883 100644 --- a/clients/probe/src/main.rs +++ b/clients/probe/src/main.rs @@ -1,10 +1,12 @@ -//! `punktfunk-probe` — the reference client for `punktfunk/1` (M3): QUIC control plane, UDP data +//! `punktfunk-probe` — the reference client for `punktfunk/1`: QUIC control plane, UDP data //! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome: //! //! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames; -//! * **stream** (`frames == 0`, virtual host): receives real NVENC AUs, writes a playable -//! `.h265`, and reports per-frame **capture→…→reassembled latency** percentiles (the host -//! stamps each frame with its capture wall clock; same-host runs share that clock). +//! * **stream** (`frames == 0`, virtual host): receives real encoded AUs, writes a playable +//! elementary stream (the dump extension follows the negotiated codec — `.h265`/`.h264`/`.av1`; +//! the probe advertises all three), and reports per-frame **capture→…→reassembled latency** +//! percentiles (the host stamps each frame with its capture wall clock; same-host runs share +//! that clock). //! //! `--input-test` exercises the input plane: scripted mouse/keyboard datagrams during the //! stream (watch them land in the host session, e.g. xev inside gamescope). `--mic-test` @@ -36,9 +38,12 @@ //! over mDNS, prints each (name, addr:port, pairing requirement, cert fingerprint to pin), and //! exits without connecting. //! -//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test] -//! [--pin HEX] [--compositor NAME] [--gamepad NAME] | --discover [SECS]` -//! (M4 adds VAAPI decode + wgpu present on this skeleton.) +//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--remode WxHxFPS:SECS] +//! [--out FILE] [--bitrate KBPS] [--codec auto|h264|hevc|av1] [--audio-channels 2|6|8] +//! [--launch APP] [--name NAME] [--speed-test KBPS:MS] +//! [--input-test | --mic-test | --touch-test | --rich-input-test] +//! [--pin HEX | --pair PIN] [--compositor NAME] [--gamepad NAME] | --discover [SECS]` +//! Env: `PUNKTFUNK_CLIENT_10BIT=1` / `PUNKTFUNK_CLIENT_444=1` advertise the 10-bit / 4:4:4 caps. use anyhow::{anyhow, Context, Result}; use punktfunk_core::config::GamepadPref; @@ -82,6 +87,10 @@ struct Args { /// multistream-decodes the host's frames and asserts the per-channel sample count, so it's the /// headless validator for the surround encode path. audio_channels: u8, + /// `--codec h264|hevc|av1|auto` — the preferred video codec (soft; the host honors it when it can + /// emit it, else falls back). The probe always advertises it can decode all three; this just sets + /// the preference byte. `auto` (default) = no preference (host decides). `0` = auto. + preferred_codec: u8, /// `--launch ID` — ask the host to launch a library title in this session (a store-qualified /// id from the host's `GET /api/v1/library`, e.g. `steam:570`). Host resolves it; `None` = none. launch: Option, @@ -210,6 +219,12 @@ fn parse_args() -> Args { .and_then(|s| s.parse().ok()) .unwrap_or(2), ), + preferred_codec: match get("--codec").unwrap_or("auto") { + "h264" | "avc" => punktfunk_core::quic::CODEC_H264, + "hevc" | "h265" => punktfunk_core::quic::CODEC_HEVC, + "av1" => punktfunk_core::quic::CODEC_AV1, + _ => 0, // auto — no preference + }, launch: get("--launch").map(str::to_string), speed_test: get("--speed-test").and_then(|s| { let (kbps, ms) = s.split_once(':')?; @@ -428,6 +443,8 @@ async fn session(args: Args) -> Result<()> { video_codecs: punktfunk_core::quic::CODEC_H264 | punktfunk_core::quic::CODEC_HEVC | punktfunk_core::quic::CODEC_AV1, + // `--codec` soft preference (0 = auto). The host honors it when it can emit it. + preferred_codec: args.preferred_codec, } .encode(), ) diff --git a/crates/pf-driver-proto/README.md b/crates/pf-driver-proto/README.md index a02b271..3f60bc0 100644 --- a/crates/pf-driver-proto/README.md +++ b/crates/pf-driver-proto/README.md @@ -4,7 +4,7 @@ The shared **host ↔ driver binary contract** for punktfunk's Windows **pf-vdis the control IOCTLs and the IDD-push frame transport, defined exactly once. It's a path dependency of **both** the host workspace ([`crates/punktfunk-host`](../punktfunk-host)) -and the out-of-workspace driver workspace ([`packaging/windows/drivers/`](../../packaging/windows)), +and the out-of-workspace driver workspace ([`packaging/windows/drivers/`](../../packaging/windows/drivers)), so it must resolve identically from either build graph. That's why it's deliberately self-contained: `no_std` (+ alloc), platform-neutral (GUID/LUID are plain integers each side converts to its own OS type), and free of `*.workspace = true` inheritance. diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs index e4dcace..eba2c95 100644 --- a/crates/punktfunk-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -844,12 +844,23 @@ pub const PUNKTFUNK_VIDEO_CAP_HDR: u8 = 0x02; /// [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.) pub const PUNKTFUNK_VIDEO_CAP_444: u8 = 0x04; +/// Codec bit for [`punktfunk_connect_ex7`] (`video_codecs` / `preferred_codec`) and the value +/// [`punktfunk_connection_codec`] returns: H.264 / AVC. (Mirrors `quic::CODEC_H264`.) +pub const PUNKTFUNK_CODEC_H264: u8 = 0x01; +/// Codec bit: H.265 / HEVC — the default codec. (Mirrors `quic::CODEC_HEVC`.) +pub const PUNKTFUNK_CODEC_HEVC: u8 = 0x02; +/// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.) +pub const PUNKTFUNK_CODEC_AV1: u8 = 0x04; + // Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift). #[cfg(feature = "quic")] const _: () = { assert!(PUNKTFUNK_VIDEO_CAP_10BIT == crate::quic::VIDEO_CAP_10BIT); assert!(PUNKTFUNK_VIDEO_CAP_HDR == crate::quic::VIDEO_CAP_HDR); assert!(PUNKTFUNK_VIDEO_CAP_444 == crate::quic::VIDEO_CAP_444); + assert!(PUNKTFUNK_CODEC_H264 == crate::quic::CODEC_H264); + assert!(PUNKTFUNK_CODEC_HEVC == crate::quic::CODEC_HEVC); + assert!(PUNKTFUNK_CODEC_AV1 == crate::quic::CODEC_AV1); }; // Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift). @@ -1160,8 +1171,8 @@ pub unsafe extern "C" fn punktfunk_connect_ex5( /// Like [`punktfunk_connect_ex5`], but additionally requests the audio channel count: /// `2` (stereo, the default behaviour of every earlier variant), `6` (5.1) or `8` (7.1). The host /// clamps the request to what it can actually capture and echoes the resolved count via -/// [`punktfunk_connection_audio_channels`]; the `0xC9` audio frames are Opus-(multi)stream encoded -/// for that layout. A client that wants surround calls this; everything else inherits stereo. +/// [`punktfunk_connection_audio_channels`]. Advertises HEVC-only with no codec preference (call +/// [`punktfunk_connect_ex7`] to negotiate the codec). /// /// # Safety /// Same as [`punktfunk_connect`]. @@ -1185,6 +1196,62 @@ pub unsafe extern "C" fn punktfunk_connect_ex6( client_cert_pem: *const std::os::raw::c_char, client_key_pem: *const std::os::raw::c_char, timeout_ms: u32, +) -> *mut PunktfunkConnection { + unsafe { + punktfunk_connect_ex7( + host, + port, + width, + height, + refresh_hz, + compositor, + gamepad, + bitrate_kbps, + video_caps, + audio_channels, + PUNKTFUNK_CODEC_HEVC, // pre-negotiation default: HEVC-only, no preference + 0, + launch_id, + pin_sha256, + observed_sha256_out, + client_cert_pem, + client_key_pem, + timeout_ms, + ) + } +} + +/// Like [`punktfunk_connect_ex6`], but additionally advertises the codecs the client can decode +/// (`video_codecs` — a bitfield of [`PUNKTFUNK_CODEC_H264`] / [`PUNKTFUNK_CODEC_HEVC`] / +/// [`PUNKTFUNK_CODEC_AV1`]) and a soft `preferred_codec` (a single codec bit, `0` = no preference). +/// The host resolves the codec it emits from these (preference honored when it can also produce it, +/// else best shared codec) and reports it via [`punktfunk_connection_codec`]. A client that omits +/// this (calls `ex6`) advertises HEVC-only, no preference — the pre-negotiation behaviour. +/// +/// # Safety +/// Same as [`punktfunk_connect`]. +#[cfg(feature = "quic")] +#[no_mangle] +#[allow(clippy::too_many_arguments)] +pub unsafe extern "C" fn punktfunk_connect_ex7( + host: *const std::os::raw::c_char, + port: u16, + width: u32, + height: u32, + refresh_hz: u32, + compositor: u32, + gamepad: u32, + bitrate_kbps: u32, + video_caps: u8, + audio_channels: u8, + video_codecs: u8, + preferred_codec: u8, + launch_id: *const std::os::raw::c_char, + pin_sha256: *const u8, + observed_sha256_out: *mut u8, + client_cert_pem: *const std::os::raw::c_char, + client_key_pem: *const std::os::raw::c_char, + timeout_ms: u32, ) -> *mut PunktfunkConnection { let r = std::panic::catch_unwind(AssertUnwindSafe(|| { if host.is_null() { @@ -1235,6 +1302,8 @@ pub unsafe extern "C" fn punktfunk_connect_ex6( bitrate_kbps, video_caps, crate::audio::normalize_channels(audio_channels), + video_codecs, + preferred_codec, launch, pin, identity, @@ -1763,6 +1832,33 @@ pub unsafe extern "C" fn punktfunk_connection_chroma_format( }) } +/// Read the video codec the host resolved for this session (from its Welcome): one of +/// [`PUNKTFUNK_CODEC_H264`] / [`PUNKTFUNK_CODEC_HEVC`] / [`PUNKTFUNK_CODEC_AV1`]. The embedder builds +/// its decoder from THIS (never assuming HEVC). `*out` is filled when non-NULL. Available +/// immediately after a successful connect (it doesn't change without a reconfigure). An older host +/// that didn't negotiate a codec reports [`PUNKTFUNK_CODEC_HEVC`]. +/// +/// # Safety +/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`. +#[cfg(feature = "quic")] +#[no_mangle] +pub unsafe extern "C" fn punktfunk_connection_codec( + c: *mut PunktfunkConnection, + out: *mut u8, +) -> PunktfunkStatus { + guard(|| { + let c = match unsafe { c.as_ref() } { + Some(c) => c, + None => return PunktfunkStatus::NullPointer, + }; + if !out.is_null() { + // SAFETY: `out` is non-null and the caller guarantees it is writable for one `u8`. + unsafe { *out = c.inner.codec }; + } + PunktfunkStatus::Ok + }) +} + /// Send one input event to the host as a QUIC datagram (non-blocking enqueue). /// /// # Safety diff --git a/crates/punktfunk-core/src/client.rs b/crates/punktfunk-core/src/client.rs index 02d28e5..396118a 100644 --- a/crates/punktfunk-core/src/client.rs +++ b/crates/punktfunk-core/src/client.rs @@ -41,8 +41,8 @@ enum CtrlRequest { /// certificate fingerprint, the resolved encoder bitrate (kbps), and the host↔client clock offset /// (ns, host minus client; 0 = no skew correction / an old host that didn't answer the handshake). /// The trailing `u8`s are the resolved encode bit depth (8/10), the chroma `chroma_format_idc` -/// (1 = 4:2:0, 3 = 4:4:4), and the resolved audio channel count (2/6/8), with [`ColorInfo`] the -/// resolved colour signalling — all from the [`Welcome`]. +/// (1 = 4:2:0, 3 = 4:4:4), the resolved audio channel count (2/6/8), and the resolved video codec +/// (`quic::CODEC_*`), with [`ColorInfo`] the resolved colour signalling — all from the [`Welcome`]. type Negotiated = ( Mode, CompositorPref, @@ -54,6 +54,7 @@ type Negotiated = ( ColorInfo, u8, u8, + u8, ); /// Accumulated state of an in-flight / finished speed test. The data-plane pump mirrors the @@ -216,6 +217,10 @@ pub struct NativeClient { /// host that omits it (→ `2`) yields working stereo. The `0xC9` audio frames are encoded with the /// matching layout. pub audio_channels: u8, + /// The video codec the host resolved and will emit ([`Welcome::codec`]) — [`quic::CODEC_H264`], + /// [`quic::CODEC_HEVC`] (default / older host), or [`quic::CODEC_AV1`]. The client builds its + /// decoder from THIS, never assuming HEVC. + pub codec: u8, } /// Pin the calling thread to the user-interactive QoS class on Apple targets. @@ -263,6 +268,11 @@ impl NativeClient { // Requested audio channel count (2 = stereo / 6 = 5.1 / 8 = 7.1); the host clamps to what it // can capture and echoes the result in [`NativeClient::audio_channels`]. audio_channels: u8, + // The codecs this client can decode (bitfield of quic::CODEC_H264 / CODEC_HEVC / CODEC_AV1) + // and the user's soft preference (a single codec bit, 0 = auto). The host resolves the codec + // it emits from these and echoes it in [`NativeClient::codec`]. + video_codecs: u8, + preferred_codec: u8, launch: Option, pin: Option<[u8; 32]>, identity: Option<(String, String)>, @@ -316,6 +326,8 @@ impl NativeClient { bitrate_kbps, video_caps, audio_channels, + video_codecs, + preferred_codec, launch, pin, identity, @@ -349,6 +361,7 @@ impl NativeClient { color, chroma_format, audio_channels, + codec, ) = match ready_rx.recv_timeout(timeout) { Ok(Ok(t)) => t, Ok(Err(e)) => return Err(e), @@ -382,6 +395,7 @@ impl NativeClient { color, chroma_format, audio_channels, + codec, }) } @@ -689,6 +703,8 @@ struct WorkerArgs { bitrate_kbps: u32, video_caps: u8, audio_channels: u8, + video_codecs: u8, + preferred_codec: u8, launch: Option, pin: Option<[u8; 32]>, identity: Option<(String, String)>, @@ -721,6 +737,8 @@ async fn worker_main(args: WorkerArgs) { bitrate_kbps, video_caps, audio_channels, + video_codecs, + preferred_codec, launch, pin, identity, @@ -789,10 +807,10 @@ async fn worker_main(args: WorkerArgs) { video_caps, // Requested surround channel count; the host echoes the resolved value in Welcome. audio_channels, - // Phase 1: the embeddable clients decode HEVC (their decoders are still HEVC-wired), - // so advertise HEVC-only until Phase 2 threads real per-client codec caps through the - // connect ABI and switches decoders on `Welcome::codec`. - video_codecs: crate::quic::CODEC_HEVC, + // The codecs this client can decode + its soft preference (0 = auto). The host + // resolves the emitted codec from these and reports it in `Welcome::codec`. + video_codecs, + preferred_codec, } .encode(), ) @@ -866,6 +884,7 @@ async fn worker_main(args: WorkerArgs) { welcome.color, welcome.chroma_format, welcome.audio_channels, + welcome.codec, )) }; @@ -884,6 +903,7 @@ async fn worker_main(args: WorkerArgs) { color, chroma_format, audio_channels, + codec, ) = match setup.await { Ok(t) => t, Err(e) => { @@ -902,6 +922,7 @@ async fn worker_main(args: WorkerArgs) { color, chroma_format, audio_channels, + codec, ))); // Input task: embedder events → QUIC datagrams. diff --git a/crates/punktfunk-core/src/quic.rs b/crates/punktfunk-core/src/quic.rs index 06bb246..a345c7b 100644 --- a/crates/punktfunk-core/src/quic.rs +++ b/crates/punktfunk-core/src/quic.rs @@ -94,6 +94,13 @@ pub struct Hello { /// clients (decodes to `0`, which [`resolve_codec`] treats as HEVC-only — every pre-negotiation /// build decoded HEVC). pub video_codecs: u8, + /// The client's *preferred* codec (a single [`CODEC_H264`] / [`CODEC_HEVC`] / [`CODEC_AV1`] bit), + /// or `0` = no preference (host decides by its own precedence). A **soft** hint: the host emits + /// it when it can also produce it (and the client advertised it in `video_codecs`), else falls + /// back to the best shared codec — see [`resolve_codec`]. Mirrors the [`Hello::compositor`] / + /// [`Hello::gamepad`] preference pattern; the resolved codec is echoed in [`Welcome::codec`]. + /// Appended after `video_codecs` as a single trailing byte. Omitted by older clients (→ `0`). + pub preferred_codec: u8, } /// [`Hello::video_caps`] bit: the client can decode a 10-bit (Main10) HEVC stream. @@ -120,12 +127,13 @@ pub const CODEC_AV1: u8 = 0x04; /// Resolve which single codec the host will emit, from the client's advertised [`Hello::video_codecs`] /// bitfield (`0` = an older client, treated as HEVC-only) intersected with what the host's chosen -/// encoder can produce (`host_capable`, also a bitfield). Precedence when several are shared: -/// **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the compatibility / -/// software floor). Returns the single-bit codec value, or `None` when the two share nothing — the -/// caller then refuses the session with a clear error rather than emitting a stream the client can't -/// decode. -pub fn resolve_codec(client_codecs: u8, host_capable: u8) -> Option { +/// encoder can produce (`host_capable`, also a bitfield). `preferred` is the client's soft preference +/// ([`Hello::preferred_codec`], `0` = none): when it's in the shared set it wins; otherwise the tie is +/// broken by **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the +/// compatibility / software floor). Returns the single-bit codec value, or `None` when client and host +/// share nothing — the caller then refuses the session with a clear error rather than emitting a +/// stream the client can't decode. +pub fn resolve_codec(client_codecs: u8, host_capable: u8, preferred: u8) -> Option { // An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent. let client = if client_codecs == 0 { CODEC_HEVC @@ -133,6 +141,13 @@ pub fn resolve_codec(client_codecs: u8, host_capable: u8) -> Option { client_codecs }; let shared = client & host_capable; + if shared == 0 { + return None; + } + // Honor the client's preference when the host can also emit it; else fall back to precedence. + if preferred != 0 && shared & preferred != 0 { + return Some(preferred); + } // Precedence: HEVC > AV1 > H.264. [CODEC_HEVC, CODEC_AV1, CODEC_H264] .into_iter() @@ -716,8 +731,13 @@ impl Hello { // present (video_caps non-zero / audio_channels not stereo) the name/launch length bytes // AND the video_caps byte must still be emitted (0 / 0) so the later byte lands at a // deterministic offset — the same discipline `launch` already imposes on `name`. + // Trailing single-byte fields, in wire order. Each is emitted when it (or ANY later field) + // carries a non-default value, so a present field always lands at a deterministic offset. + let ac_present = self.audio_channels != 2; + let vcodecs_present = self.video_codecs != 0; + let pref_present = self.preferred_codec != 0; let need_placeholders = - self.video_caps != 0 || self.audio_channels != 2 || self.video_codecs != 0; + self.video_caps != 0 || ac_present || vcodecs_present || pref_present; match (&self.name, &self.launch) { (None, None) if !need_placeholders => {} (name, _) => { @@ -734,17 +754,21 @@ impl Hello { } // video_caps: single trailing byte. Emitted when non-zero OR when a later field follows (so // that field lands at a deterministic offset right after it). - if self.video_caps != 0 || self.audio_channels != 2 || self.video_codecs != 0 { + if self.video_caps != 0 || ac_present || vcodecs_present || pref_present { b.push(self.video_caps); } - // audio_channels: single trailing byte. Emitted when non-stereo OR when video_codecs follows. - if self.audio_channels != 2 || self.video_codecs != 0 { + // audio_channels: emitted when non-stereo OR a later field follows. + if ac_present || vcodecs_present || pref_present { b.push(self.audio_channels); } - // video_codecs: single trailing byte. Last field; omitted when `0` (older client → HEVC-only). - if self.video_codecs != 0 { + // video_codecs: emitted when non-zero OR preferred_codec follows. + if vcodecs_present || pref_present { b.push(self.video_codecs); } + // preferred_codec: single trailing byte. Last field; omitted when `0` (no preference). + if pref_present { + b.push(self.preferred_codec); + } b } @@ -825,6 +849,15 @@ impl Hello { let video_caps_off = launch_off + 1 + launch_len; b.get(video_caps_off + 2).copied().unwrap_or(0) }, + // Optional trailing preferred-codec byte, one past video_codecs. Absent on an older + // client → `0` (no preference; the host decides by precedence). + preferred_codec: { + let name_len = b.get(26).copied().unwrap_or(0) as usize; + let launch_off = 27 + name_len; + let launch_len = b.get(launch_off).copied().unwrap_or(0) as usize; + let video_caps_off = launch_off + 1 + launch_len; + b.get(video_caps_off + 3).copied().unwrap_or(0) + }, }) } } @@ -2025,21 +2058,41 @@ mod tests { #[test] fn codec_negotiation_and_back_compat() { - // resolve_codec precedence (HEVC > AV1 > H.264) and the no-shared-codec refusal. + // resolve_codec precedence (HEVC > AV1 > H.264), no preference (0). assert_eq!( - resolve_codec(CODEC_H264 | CODEC_HEVC, CODEC_HEVC | CODEC_AV1), + resolve_codec(CODEC_H264 | CODEC_HEVC, CODEC_HEVC | CODEC_AV1, 0), Some(CODEC_HEVC) ); assert_eq!( - resolve_codec(CODEC_H264 | CODEC_AV1, CODEC_AV1 | CODEC_H264), + resolve_codec(CODEC_H264 | CODEC_AV1, CODEC_AV1 | CODEC_H264, 0), Some(CODEC_AV1) ); - assert_eq!(resolve_codec(CODEC_H264, CODEC_H264), Some(CODEC_H264)); + assert_eq!(resolve_codec(CODEC_H264, CODEC_H264, 0), Some(CODEC_H264)); // A software host (H.264 only) + an HEVC-only client share nothing → refuse. - assert_eq!(resolve_codec(CODEC_HEVC, CODEC_H264), None); + assert_eq!(resolve_codec(CODEC_HEVC, CODEC_H264, 0), None); // An older client (0 = no codec byte) is treated as HEVC-only. - assert_eq!(resolve_codec(0, CODEC_HEVC | CODEC_H264), Some(CODEC_HEVC)); - assert_eq!(resolve_codec(0, CODEC_H264), None); + assert_eq!( + resolve_codec(0, CODEC_HEVC | CODEC_H264, 0), + Some(CODEC_HEVC) + ); + assert_eq!(resolve_codec(0, CODEC_H264, 0), None); + + // Soft preference: honored when the host can also emit it, overriding precedence... + assert_eq!( + resolve_codec(CODEC_H264 | CODEC_HEVC, CODEC_H264 | CODEC_HEVC, CODEC_H264), + Some(CODEC_H264) + ); + assert_eq!( + resolve_codec(CODEC_HEVC | CODEC_AV1, CODEC_HEVC | CODEC_AV1, CODEC_AV1), + Some(CODEC_AV1) + ); + // ...but falls back to precedence when the preferred codec isn't in the shared set. + assert_eq!( + resolve_codec(CODEC_HEVC | CODEC_H264, CODEC_HEVC | CODEC_H264, CODEC_AV1), + Some(CODEC_HEVC) + ); + // A preference the host can't emit still can't rescue a no-shared-codec case. + assert_eq!(resolve_codec(CODEC_HEVC, CODEC_H264, CODEC_HEVC), None); // A Hello advertising codecs roundtrips, and the wire form of a codec-only Hello decodes on // a build that ignores the trailing byte (back-compat: extra bytes are skipped). @@ -2058,15 +2111,23 @@ mod tests { video_caps: 0, audio_channels: 2, // stereo — forces the video_caps/audio_channels placeholders video_codecs: CODEC_H264 | CODEC_HEVC, + preferred_codec: CODEC_H264, }; let enc = h.encode(); + let dec = Hello::decode(&enc).unwrap(); + assert_eq!(dec.video_codecs, CODEC_H264 | CODEC_HEVC); + assert_eq!(dec.preferred_codec, CODEC_H264); + // Drop the preferred_codec byte → still decodes, video_codecs intact, preference gone. + let no_pref = &enc[..enc.len() - 1]; assert_eq!( - Hello::decode(&enc).unwrap().video_codecs, + Hello::decode(no_pref).unwrap().video_codecs, CODEC_H264 | CODEC_HEVC ); - // A pre-codec Hello (no trailing codec byte) decodes to 0 → HEVC-only via resolve_codec. - let legacy = &enc[..enc.len() - 1]; // drop the codec byte (it was the last field) + assert_eq!(Hello::decode(no_pref).unwrap().preferred_codec, 0); + // A pre-codec Hello (no video_codecs/preferred bytes) decodes to 0 → HEVC-only. + let legacy = &enc[..enc.len() - 2]; assert_eq!(Hello::decode(legacy).unwrap().video_codecs, 0); + assert_eq!(Hello::decode(legacy).unwrap().preferred_codec, 0); // A pre-codec Welcome (no codec byte) decodes to HEVC. let mut w = Welcome::decode( @@ -2145,6 +2206,7 @@ mod tests { video_caps: VIDEO_CAP_10BIT, audio_channels: 2, video_codecs: CODEC_H264 | CODEC_HEVC, // exercise the codec bitfield roundtrip + preferred_codec: CODEC_HEVC, }; assert_eq!(Hello::decode(&h.encode()).unwrap(), h); let s = Start { @@ -2226,6 +2288,7 @@ mod tests { video_caps: 0, audio_channels: 2, video_codecs: 0, + preferred_codec: 0, }; let enc = h.encode(); assert_eq!(enc.len(), 26); @@ -2332,6 +2395,7 @@ mod tests { video_caps: 0, audio_channels: 2, video_codecs: 0, + preferred_codec: 0, }; let enc = base.encode(); assert_eq!( @@ -2381,6 +2445,7 @@ mod tests { video_caps: 0, audio_channels: 2, video_codecs: 0, + preferred_codec: 0, }; // launch alone (no name): a zero-length name placeholder keeps the offset deterministic. let with_launch = Hello { @@ -2589,6 +2654,7 @@ mod tests { video_caps: 0, audio_channels: 2, video_codecs: 0, + preferred_codec: 0, } .encode(); assert!(PairRequest::decode(&h).is_err(), "abi {abi} parsed as pair"); diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index e0a255d..15e1962 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -663,8 +663,9 @@ async fn serve_session( // client's advertised codecs). A GPU-less software host emits H.264, so an HEVC-only client // shares nothing with it → refuse honestly rather than send a stream it can't decode. let host_codecs = crate::encode::Codec::host_wire_caps(); - let codec_bit = punktfunk_core::quic::resolve_codec(hello.video_codecs, host_codecs) - .ok_or_else(|| { + let codec_bit = + punktfunk_core::quic::resolve_codec(hello.video_codecs, host_codecs, hello.preferred_codec) + .ok_or_else(|| { anyhow!( "no shared video codec: client advertised 0x{:02x}, host can emit 0x{:02x} \ (a software-encode host produces H.264 — the client must advertise CODEC_H264)", @@ -3976,6 +3977,8 @@ mod tests { 0, 0, // video_caps 2, // audio_channels (stereo) + 0, // video_codecs (0 → HEVC-only) + 0, // preferred_codec (auto) None, // launch None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client Some((cert, key)), @@ -4036,6 +4039,8 @@ mod tests { 0, 0, // video_caps 2, // audio_channels (stereo) + 0, // video_codecs + 0, // preferred_codec None, // launch None, None, @@ -4062,6 +4067,8 @@ mod tests { 0, 0, // video_caps 2, // audio_channels (stereo) + 0, // video_codecs + 0, // preferred_codec None, // launch Some(host_fp), Some((cert.clone(), key.clone())), diff --git a/design/README.md b/design/README.md index 77ab739..dc97608 100644 --- a/design/README.md +++ b/design/README.md @@ -14,12 +14,14 @@ holds the full originals. | [`implementation-plan.md`](implementation-plan.md) | Master design thesis (why GF(2¹⁶) FEC + Linux virtual displays; three-phase de-risking), architecture invariants, latency budget, risk register | **Design reference** — §0–7,9 kept; milestones → CLAUDE.md | | [`apollo-comparison.md`](apollo-comparison.md) | Apollo↔punktfunk architecture map + file index + ~63-item transferable-improvement backlog (Windows-host focus) | **Reference + open backlog** — ~⅓ shipped (collapsed); rest open | | [`security-review.md`](security-review.md) | Whole-project security audit (2026-06-21), 12 findings | **Audit trail** — 11 fixed/inherent; **#12 open** | +| [`security-review-2026-06-28.md`](security-review-2026-06-28.md) | Host-scoped follow-up audit (18 attack surfaces; 15 confirmed + 9 partial) | **Audit trail** — fixes landed; deferred/accepted items recorded | | [`ci.md`](ci.md) | CI/CD architecture: Gitea workflows, runners, release model, signing | **Evergreen reference** | | [`linux-setup.md`](linux-setup.md) | Linux host bring-up (NVIDIA/headless) + troubleshooting | **Setup guide** (evergreen) | | [`gamestream-host-plan.md`](gamestream-host-plan.md) | GameStream/Moonlight-compat host (P1.1–P1.6) | **Shipped** — stub + the 2 deferral decisions | | [`stats-capture-plan.md`](stats-capture-plan.md) | Web-console performance capture | **Shipped** — stub | | [`session-aware-host-followups.md`](session-aware-host-followups.md) | Session-aware host known limitations | **Open items** — #2/#3 shipped; #1,#4–8 parked | -| [`gamescope-multiuser.md`](gamescope-multiuser.md) | Per-session gamescope isolation (the 4 plumbing items) | **Deferred** — reference spec | +| [`gamescope-multiuser.md`](gamescope-multiuser.md) | Per-session gamescope isolation (the 4 plumbing items) | **Superseded** — absorbed by `multi-user-profiles.md` | +| [`multi-user-profiles.md`](multi-user-profiles.md) | Multi-user / profiles end to end: map a client to a real host OS user account (own isolated desktop), web-console config, per-profile passcode | **Design, schema-of-record** — not yet implemented | | [`host-latency-plan.md`](host-latency-plan.md) | Latency under GPU contention — 4-tier plan | **Partly shipped** — superseded by ↓; diagnostics + open tiers kept | | [`gpu-contention-investigation.md`](gpu-contention-investigation.md) | GPU-contention root-cause + ranked levers (supersedes ↑) | **Active plan** — §5.A shipped; §5.B/C/E/F/G open | | [`hdr-pipeline-plan.md`](hdr-pipeline-plan.md) | Glass-to-glass HDR | **Steps 0–3 shipped**; Step 4 (Linux) open | @@ -34,7 +36,8 @@ holds the full originals. | [`apple-stage2-presenter.md`](apple-stage2-presenter.md) | Apple stage-2 (VTDecompressionSession + CAMetalLayer) presenter | **Shipped (opt-in)** — make-default + iOS open | | [`game-library-stores.md`](game-library-stores.md) | Multi-store game library | **Phases 1–4 shipped** — 6 providers + 8 Qs open | | [`dualsense-haptics.md`](dualsense-haptics.md) | DualSense advanced-haptics feasibility | **HID shipped**; audio haptics deferred (3 walls) | -| [`steam-controller-deck-support.md`](steam-controller-deck-support.md) | Rich Steam Controller / Steam Deck **input fidelity** (paddles · trackpads · gyro → virtual `hid-steam`) | **Design + M0 GREEN** (Linux bind proven); M1+ open | +| [`steam-controller-deck-support.md`](steam-controller-deck-support.md) | Rich Steam Controller / Steam Deck **input fidelity** (paddles · trackpads · gyro → virtual `hid-steam`) | **M0–M6 green** — full pipeline + fallback + conflict gate built; validation items open | +| [`steam-deck-passthrough-plan.md`](steam-deck-passthrough-plan.md) | Shippable virtual Deck on any Linux host (usbip/`vhci_hcd` + raw_gadget transport ladder) + client leave-shortcuts | **Built, CI-green** — on-glass validation open | | [`controller-only-mode.md`](controller-only-mode.md) | Controller-only **session shape** — Deck/desktop as a remote gamepad, no video/audio (complements ↑) | **Design** — not yet implemented | | [`archive/windows-secure-desktop.md`](archive/windows-secure-desktop.md) | Two-process WGC secure-desktop design | **Archived** — shipped but now a fallback (IDD-push primary) | @@ -77,11 +80,12 @@ owning doc.) - 6 remaining providers (Desktop/Flatpak, itch.io, Ubisoft Connect, Amazon Games, Battle.net, EA app); the `/library/art//` mgmt endpoint; refactor `library.rs` into a `library/` dir; 8 open design questions; optional SteamGridDB v2 enrichment. → `game-library-stores` **Controllers / input** -- Rich Steam Controller / Steam Deck capture + virtual `hid-steam` inject (M1+ — Linux UHID, then clients, then deferred Windows UMDF). → `steam-controller-deck-support` +- Steam Controller / Steam Deck remaining validation: Moonlight paddle regression + a live SDL-game consume test. → `steam-controller-deck-support` +- usbip virtual-Deck on-glass validation (non-SteamOS promote-in-game + Deck raw_gadget regression). → `steam-deck-passthrough-plan` - Controller-only session shape (Deck/desktop as a remote gamepad, no video/audio) — `session_flags`/`SESSION_INPUT_ONLY` protocol bit + host skip-data-plane branch + client controller-only path. → `controller-only-mode` **Multi-user / sessions** -- gamescope per-session input/audio isolation (independent desktops) — the 4 plumbing items, deferred. → `gamescope-multiuser`, `implementation-plan` +- Magic multi-user / profiles — the full client→host-OS-user design is written (schema-of-record, absorbs the gamescope isolation plumbing); implementation not started. → `multi-user-profiles` **Security** - **#12** — scope `NODE_TLS_REJECT_UNAUTHORIZED` to a per-request pinned agent (needs `bun add undici`); latent-only today, but **must fix before the web app gains any off-loopback server-side TLS**. → `security-review` diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index 7153993..c3dc45b 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -38,7 +38,7 @@ On Linux the host **rewrites `WAYLAND_DISPLAY` / `XDG_CURRENT_DESKTOP` / `XDG_RU | `PUNKTFUNK_VIDEO_SOURCE` | `virtual` · `portal` | `virtual` creates a per-client display at the client's exact mode (the normal choice). `portal` captures an existing monitor instead. | | `PUNKTFUNK_ZEROCOPY` | `1` · `0` | GPU zero-copy capture→encode (dmabuf → CUDA → NVENC, or D3D11 on Windows). Leave on; it falls back to a CPU path automatically. | | `PUNKTFUNK_INPUT_BACKEND` | `libei` · `gamescope` · `wlr` · `uinput` | How input is injected. `libei` for GNOME/KDE, `gamescope` for Bazzite/gamescope, `wlr` for Sway/wlroots. Auto-detected with the compositor. | -| `PUNKTFUNK_ENCODER` | `auto` · `nvenc` · `vaapi` (Linux) · `amf` · `qsv` · `sw` (Windows) | Encoder backend. `auto` (default) detects the GPU vendor: NVIDIA→NVENC, AMD→VAAPI/AMF, Intel→VAAPI/QSV, else software. | +| `PUNKTFUNK_ENCODER` | `auto` · `nvenc` · `vaapi` (Linux) · `amf` · `qsv` (Windows) · `software` | Encoder backend. `auto` (default) detects the GPU vendor: NVIDIA→NVENC, AMD→VAAPI/AMF, Intel→VAAPI/QSV. `software` (aliases `sw`/`openh264`) is the GPU-less H.264 path on both platforms — on Windows `auto` falls back to it when no GPU is found; on Linux it is **explicit-only** (`auto` never picks it). | | `PUNKTFUNK_RENDER_NODE` | path | Linux DRM render node for zero-copy (default `/dev/dri/renderD128`). Set on multi-GPU boxes to pick the right GPU. | Resolution and refresh are **not** set here — **the client chooses them.** When a device connects, @@ -76,6 +76,7 @@ picture. | `PUNKTFUNK_10BIT` | `1` | HEVC Main10 / HDR. Honored only when the client also advertises 10-bit. **Windows host only** (the Linux host stays 8-bit). | | `PUNKTFUNK_444` | `1` | Full-chroma HEVC 4:4:4 (Range Extensions) — sharper text/desktop, no chroma loss. **punktfunk/1 native only** (Moonlight stays 4:2:0), HEVC-only, honored only when the client advertises 4:4:4 **and** the GPU supports it (probed; NVENC is the validated path — VAAPI/AMF/QSV decline). Independent of 10-bit. | | `PUNKTFUNK_DSCP` | `1` | Opt-in DSCP / `SO_PRIORITY` QoS tagging on the media sockets. No-op on the wire on Windows without a qWAVE policy. | +| `PUNKTFUNK_OH264_THREADS` / `PUNKTFUNK_OH264_GOP` | `N` | Software (openh264) encoder tuning: encode threads (default 2 — latency over throughput) and GOP length (default 0 = encoder-auto). Only relevant with `PUNKTFUNK_ENCODER=software`. | ## Gamepads @@ -151,13 +152,14 @@ good value: ## Multiple devices at once -Today the native `punktfunk/1` host (`serve`) streams **one session at a time** — additional clients -wait in the accept queue until the active session ends. Each session gets its own virtual display at -the client's exact resolution; concurrent native sessions are on the roadmap. (`punktfunk1-host`, the -standalone test host, has a `--max-concurrent N` knob — see the [Host CLI](/docs/host-cli) reference — -but `serve` does not take that flag.) +The native `punktfunk/1` host (`serve`) streams up to **4 sessions at once** by default (an encoder +bound); further clients wait in the accept queue until a slot frees up. Each session gets its own +virtual display at the client's exact resolution, sharing the host's input/audio/mic services. The +limit isn't settable from `serve`'s command line yet — `punktfunk1-host`, the standalone test host, +exposes it as `--max-concurrent N` (see the [Host CLI](/docs/host-cli) reference). ## Codec and FEC -- The host encodes **HEVC (H.265)** by default; **AV1** is available for clients that support it. +- Client and host **negotiate the codec**: **HEVC (H.265)** by default, **AV1** for clients that + support it, and **H.264** when the session runs on the GPU-less software encoder. - The native protocol adds forward error correction for lossy links — see `PUNKTFUNK_FEC_PCT` above. diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md index a7a5e72..e61873a 100644 --- a/docs-site/content/docs/host-cli.md +++ b/docs-site/content/docs/host-cli.md @@ -67,8 +67,8 @@ punktfunk-host punktfunk1-host --source virtual | `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). | `--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`punktfunk1-host`-only** — `serve` does not -accept them. On `serve` you arm pairing from the web console instead, and concurrency is not -yet capped from the command line. +accept them. On `serve` you arm pairing from the web console instead, and concurrency is fixed at +the built-in default (4 sessions) rather than settable from the command line. Both `serve` and `punktfunk1-host` advertise the host on the network so clients can discover it. List hosts from another machine with `punktfunk-probe --discover`. diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 79618e7..9dd4c75 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -28,6 +28,7 @@ "forgot-password", "---Project---", "roadmap", + "status", "channels", "---Reference---", "[API Reference](/api)" diff --git a/docs-site/content/docs/requirements.md b/docs-site/content/docs/requirements.md index 8c7b58d..e55145b 100644 --- a/docs-site/content/docs/requirements.md +++ b/docs-site/content/docs/requirements.md @@ -5,7 +5,8 @@ description: What you need to run a punktfunk host — GPU, driver, desktop, and ## Supported setups -A punktfunk host runs primarily on a Linux machine with an NVIDIA GPU (a native +A punktfunk host runs primarily on a Linux machine with a dedicated GPU — NVIDIA (NVENC) is the +most-exercised path, and AMD/Intel GPUs work via VAAPI (a native [Windows host](/docs/windows-host) is also available — see below). These are the Linux desktop environments it supports today, each with its own guide: @@ -32,6 +33,10 @@ listed, the host still needs one of these compositor backends to create a virtua not just `nvidia-utils` — without it the compositor can't initialise the GPU and capture fails. Each setup guide installs the right package (e.g. `libnvidia-gl-` on Ubuntu). - **`nvidia-drm modeset=1`** must be enabled (Wayland on NVIDIA needs it). The setup guides cover this. +- **AMD / Intel GPUs** encode via **VAAPI** instead (install `mesa-va-drivers` or + `intel-media-driver`; validated live on AMD RDNA3). The NVIDIA-specific notes above don't apply + there. A GPU-less software H.264 encoder also exists (`PUNKTFUNK_ENCODER=software`), meant as a + fallback rather than a daily driver. > Consumer GeForce cards historically cap the number of **concurrent** NVENC sessions (a few at once); > workstation cards don't. This only matters if you stream to many devices simultaneously. diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index 058e4b6..79dcdde 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -52,8 +52,8 @@ see [Status & Progress](/docs/status). - **Windows client on-glass validation.** The hardware (D3D11VA) decode, HDR present, and GUI are built and ship as a signed MSIX — they just need verification on real GPU hardware. -- **Apple stage-2 presenter as the default.** The lower-latency `VTDecompressionSession` → - `CAMetalLayer` path is live behind an opt-in flag and graduating to the default. +- **Apple presenter polish.** The lower-latency `VTDecompressionSession` → `CAMetalLayer` stage-2 + path is now the default; HDR brightness and 4:4:4 still need on-glass validation. - **Web console parity.** Surfacing the speed test and bitrate picker the apps already have. - **Windows host hardening.** Broader real-world testing — especially on-glass validation of the AMD (AMF) and Intel (QSV) encode paths, which are CI-green but newer than NVENC. diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md index 2ab2c17..aaad3de 100644 --- a/docs-site/content/docs/status.md +++ b/docs-site/content/docs/status.md @@ -15,7 +15,7 @@ A high-level view of where punktfunk stands. The ordered plan of work is on the | **GameStream host** (Moonlight-compatible) | ✅ working end-to-end; HDR/surround-audio polish open | | **Native protocol** — `punktfunk/1` (QUIC control + UDP data, GF(2¹⁶) Leopard FEC + AES-GCM) | ✅ full session planes, validated live | | **Windows host** (x64) | 🟡 implemented & shipping as a signed installer; NVIDIA/AMD/Intel encode, newer than the Linux host | -| **macOS / iOS / iPadOS / tvOS client** | ✅ full client; on-glass stage-2 presenter behind an opt-in flag, becoming the default | +| **macOS / iOS / iPadOS / tvOS client** | ✅ full client; on-glass-validated stage-2 presenter is the default | | **Linux client** (`punktfunk-client`, GTK4/libadwaita) | ✅ full client; VAAPI zero-copy decode + software fallback | | **Windows client** (`punktfunk-client`, WinUI 3) | ✅ stage 1 complete; ships as signed MSIX; on-glass hardware validation pending | | **Android client** (phone + Android TV) | ✅ full client; hardware HEVC decode + HDR10 | @@ -35,7 +35,7 @@ host is newer than the Linux host.) **gamescope**, **Mutter**, and **Sway/wlroots**. - **Zero-copy GPU pipeline.** Captured frames stay on the GPU (dmabuf → CUDA → NVENC) with automatic split-encode at very high resolutions. Stable 240 fps at 5120×1440 has been - measured. + measured. A GPU-less software H.264 encoder exists as an explicit fallback. - **HDR (10-bit), on the Windows host.** An HDR Windows desktop is captured and encoded as HEVC Main10 (BT.2020 PQ) to HDR-capable clients (Windows, Android). Linux hosts stream 8-bit for now — HDR there is blocked upstream at the compositor. @@ -55,7 +55,7 @@ host is newer than the Linux host.) | Client | Highlights | |---|---| -| **macOS / iOS / iPadOS / tvOS** | VideoToolbox HEVC decode, GameController capture, full DualSense feedback, mDNS discovery, PIN pairing + TOFU, network speed test, latency HUD. Stage-2 presenter (`VTDecompressionSession` → `CAMetalLayer`, ~11 ms p50 capture→present) is built and validated on glass behind an opt-in flag, becoming the default. Ships as one universal TestFlight build / App Store listing. | +| **macOS / iOS / iPadOS / tvOS** | VideoToolbox HEVC decode, GameController capture, full DualSense feedback, mDNS discovery, PIN pairing + TOFU, network speed test, latency HUD. Stage-2 presenter (`VTDecompressionSession` → `CAMetalLayer`, ~11 ms p50 capture→present) is validated on glass and is the default (stage 1 remains the fallback when Metal is unavailable). Ships as one universal TestFlight build / App Store listing. | | **Linux** (`punktfunk-client`) | GTK4/libadwaita. FFmpeg decode with VAAPI → DRM-PRIME dmabuf zero-copy (Intel/AMD; software fallback on NVIDIA), PipeWire audio + mic, SDL3 gamepads incl. DualSense, mDNS discovery, PIN pairing + TOFU, speed test. Ships as Flatpak, apt, rpm, and Arch packages. | | **Windows** (`punktfunk-client`) | WinUI 3. D3D11VA zero-copy decode, HDR10, WASAPI audio + mic, SDL3 gamepads incl. DualSense, mDNS discovery, and the full PIN/TOFU trust surface are all implemented. Ships as a signed MSIX (x86_64 + ARM64). **Stage 1 complete; D3D11VA decode, HDR present, and the GUI are pending on-glass validation on real GPU hardware.** | | **Android** (phone + Android TV) | Kotlin app with a Rust core over JNI. NDK `AMediaCodec` hardware HEVC decode + HDR10 (Main10/BT.2020 PQ), Opus/Oboe audio + mic, gamepad input with rumble/HID feedback, mDNS discovery, PIN pairing + TOFU (Keystore identity), live stats HUD, and D-pad/controller focus navigation for TV. Ships to the Google Play Internal Testing track. | @@ -113,7 +113,6 @@ See the [Roadmap](/docs/roadmap) for the ordered list. Near-term: - **True glass-to-glass latency** — combine the client present-stamp (decode → present) with the host render → capture term for a complete end-to-end number. -- **Make the Apple stage-2 presenter the default** after a few more resolution/HDR checks. - **On-glass validation of the Windows client** (D3D11VA decode, HDR present, GUI) on real GPU hardware. - **gamescope multi-user isolation** — per-session input/audio so concurrent sessions can diff --git a/docs-site/content/docs/steam-deck.md b/docs-site/content/docs/steam-deck.md index 225485e..ceafcfa 100644 --- a/docs-site/content/docs/steam-deck.md +++ b/docs-site/content/docs/steam-deck.md @@ -96,5 +96,3 @@ The check follows the [channel](/docs/channels) you installed from: a plugin ins The plugin source lives in [`clients/decky`](https://git.unom.io/unom/punktfunk/src/branch/main/clients/decky/README.md). - - diff --git a/docs-site/content/docs/troubleshooting.md b/docs-site/content/docs/troubleshooting.md index a051b52..0d117bb 100644 --- a/docs-site/content/docs/troubleshooting.md +++ b/docs-site/content/docs/troubleshooting.md @@ -69,9 +69,9 @@ Then log out and back in. On other distros this is `sudo usermod -aG input $USER clients' [speed test](/docs/configuration#bitrate) picks a safe value; with Moonlight, set it manually. - Prefer a **wired** connection or 5 GHz Wi-Fi between host and client. -- Streaming to **many devices at once** shares the GPU encoder. The production host - (`serve`) handles one native session at a time, with extra clients queued; heavy load is - usually bitrate-bound, so lower the bitrate first. +- Streaming to **many devices at once** shares the GPU encoder. The host serves several + concurrent native sessions (up to 4 by default); heavy load is usually bitrate-bound, so + lower the bitrate first. ## Still stuck? diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index 08eaa4c..cd2226e 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -133,6 +133,16 @@ // [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.) #define PUNKTFUNK_VIDEO_CAP_444 4 +// Codec bit for [`punktfunk_connect_ex7`] (`video_codecs` / `preferred_codec`) and the value +// [`punktfunk_connection_codec`] returns: H.264 / AVC. (Mirrors `quic::CODEC_H264`.) +#define PUNKTFUNK_CODEC_H264 1 + +// Codec bit: H.265 / HEVC — the default codec. (Mirrors `quic::CODEC_HEVC`.) +#define PUNKTFUNK_CODEC_HEVC 2 + +// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.) +#define PUNKTFUNK_CODEC_AV1 4 + // 16-byte AEAD authentication tag appended by GCM. #define TAG_LEN 16 @@ -986,8 +996,8 @@ PunktfunkConnection *punktfunk_connect_ex5(const char *host, // Like [`punktfunk_connect_ex5`], but additionally requests the audio channel count: // `2` (stereo, the default behaviour of every earlier variant), `6` (5.1) or `8` (7.1). The host // clamps the request to what it can actually capture and echoes the resolved count via -// [`punktfunk_connection_audio_channels`]; the `0xC9` audio frames are Opus-(multi)stream encoded -// for that layout. A client that wants surround calls this; everything else inherits stereo. +// [`punktfunk_connection_audio_channels`]. Advertises HEVC-only with no codec preference (call +// [`punktfunk_connect_ex7`] to negotiate the codec). // // # Safety // Same as [`punktfunk_connect`]. @@ -1009,6 +1019,36 @@ PunktfunkConnection *punktfunk_connect_ex6(const char *host, uint32_t timeout_ms); #endif +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Like [`punktfunk_connect_ex6`], but additionally advertises the codecs the client can decode +// (`video_codecs` — a bitfield of [`PUNKTFUNK_CODEC_H264`] / [`PUNKTFUNK_CODEC_HEVC`] / +// [`PUNKTFUNK_CODEC_AV1`]) and a soft `preferred_codec` (a single codec bit, `0` = no preference). +// The host resolves the codec it emits from these (preference honored when it can also produce it, +// else best shared codec) and reports it via [`punktfunk_connection_codec`]. A client that omits +// this (calls `ex6`) advertises HEVC-only, no preference — the pre-negotiation behaviour. +// +// # Safety +// Same as [`punktfunk_connect`]. +PunktfunkConnection *punktfunk_connect_ex7(const char *host, + uint16_t port, + uint32_t width, + uint32_t height, + uint32_t refresh_hz, + uint32_t compositor, + uint32_t gamepad, + uint32_t bitrate_kbps, + uint8_t video_caps, + uint8_t audio_channels, + uint8_t video_codecs, + uint8_t preferred_codec, + const char *launch_id, + const uint8_t *pin_sha256, + uint8_t *observed_sha256_out, + const char *client_cert_pem, + const char *client_key_pem, + uint32_t timeout_ms); +#endif + #if defined(PUNKTFUNK_FEATURE_QUIC) // Generate a persistent client identity: a self-signed certificate + private key, both // PEM, NUL-terminated, written into the caller's buffers. Generate ONCE, store both @@ -1179,6 +1219,19 @@ PunktfunkStatus punktfunk_connection_color_info(PunktfunkConnection *c, PunktfunkStatus punktfunk_connection_chroma_format(PunktfunkConnection *c, uint8_t *out); #endif +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Read the video codec the host resolved for this session (from its Welcome): one of +// [`PUNKTFUNK_CODEC_H264`] / [`PUNKTFUNK_CODEC_HEVC`] / [`PUNKTFUNK_CODEC_AV1`]. The embedder builds +// its decoder from THIS (never assuming HEVC). `*out` is filled when non-NULL. Available +// immediately after a successful connect (it doesn't change without a reconfigure). An older host +// that didn't negotiate a codec reports [`PUNKTFUNK_CODEC_HEVC`]. +// +// # Safety +// `c` is a valid connection handle; `out` is NULL or writable for one `u8`. +PunktfunkStatus punktfunk_connection_codec(PunktfunkConnection *c, + uint8_t *out); +#endif + #if defined(PUNKTFUNK_FEATURE_QUIC) // Send one input event to the host as a QUIC datagram (non-blocking enqueue). // diff --git a/packaging/README.md b/packaging/README.md index 0a74402..b12bd56 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -1,8 +1,9 @@ # Packaging punktfunk for Fedora / Bazzite -The punktfunk host is Linux-only and links system FFmpeg (NVENC), PipeWire, Opus and -the NVIDIA driver. This directory packages it for the **Fedora Atomic / Bazzite** world -(rpm-ostree + bootc), where most of those deps are already present. +The punktfunk host links system FFmpeg (NVENC on NVIDIA, VAAPI on AMD/Intel, with a GPU-less +software-H.264 fallback), PipeWire and Opus. This page covers packaging it for the +**Fedora Atomic / Bazzite** world (rpm-ostree + bootc), where most of those deps are already +present; the NVIDIA-specific notes below apply to the NVENC path. > 👉 **Ubuntu/Debian hosts** install via `apt` from Gitea's package registry — see > [`debian/README.md`](debian/README.md) (`apt update && apt upgrade` for new builds). @@ -20,6 +21,10 @@ packaging/ copr/ # COPR build-from-SCM settings ``` +The other packaging targets have their own READMEs: [`debian/`](debian/README.md) (apt), +[`arch/`](arch/README.md) (PKGBUILD + sysext), [`flatpak/`](flatpak/README.md) (the client), +[`windows/`](windows/README.md) (host installer + drivers), plus `kde/` and `linux/` helpers. + ## What's needed beyond base Fedora | Dependency | Where it comes from | @@ -121,7 +126,8 @@ An RPM (or the bootc layer) installs into the host system where those just work. ## Building the SRPM/RPM locally (Fedora only) ```sh -git archive --format=tar.gz --prefix=punktfunk-0.0.1/ -o ~/rpmbuild/SOURCES/punktfunk-0.0.1.tar.gz HEAD +git archive --format=tar.gz --prefix=punktfunk-0.3.0/ -o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD rpmbuild -ba packaging/rpm/punktfunk.spec # needs the BuildRequires from the spec +# (0.3.0 = the spec's default %{pf_version}; the prefix and tarball name must match it) ``` (Not buildable on Debian/Ubuntu — use a Fedora toolbox/container or COPR.) diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index 9d1f004..ff4097b 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -404,8 +404,8 @@ Debian/Ubuntu — the host links system FFmpeg/PipeWire and won't build there), `packaging/README.md`: ```sh -git archive --format=tar.gz --prefix=punktfunk-0.0.1/ \ - -o ~/rpmbuild/SOURCES/punktfunk-0.0.1.tar.gz HEAD +git archive --format=tar.gz --prefix=punktfunk-0.3.0/ \ + -o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD # 0.3.0 = the spec's default version rpmbuild -ba packaging/rpm/punktfunk.spec # needs the spec's BuildRequires + RPM Fusion ``` diff --git a/packaging/debian/README.md b/packaging/debian/README.md index 6c251ec..13eead2 100644 --- a/packaging/debian/README.md +++ b/packaging/debian/README.md @@ -2,7 +2,7 @@ `punktfunk-host` is published as a `.deb` to **Gitea's Debian package registry** in the public `unom` org, so the Ubuntu hosts update with plain `apt`. CI (`.gitea/workflows/deb.yml`) builds -and publishes on every push to `main` (a rolling `0.3.0~ciN.g` build to the **`canary`** apt +and publishes on every push to `main` (a rolling `0.5.0~ciN.g` build to the **`canary`** apt distribution) and on `vX.Y.Z` tags (a clean `X.Y.Z` to the **`stable`** distribution, plus attached to the unified Gitea Release). The two are separate apt distributions, so a stable box never jumps to a canary build — see [Release Channels](https://punktfunk.unom.io/docs/channels). The repo line diff --git a/packaging/rpm/README.md b/packaging/rpm/README.md index 63c4451..636c8a9 100644 --- a/packaging/rpm/README.md +++ b/packaging/rpm/README.md @@ -3,7 +3,7 @@ `punktfunk-host` is published as an RPM to **Gitea's RPM package registry** in the public `unom` org (stable groups `bazzite`/`fedora-44`, canary groups `bazzite-canary`/`fedora-44-canary`), so Bazzite / Fedora Atomic hosts layer and update it with `rpm-ostree`. CI (`.gitea/workflows/rpm.yml`) -builds and publishes on every push to `main` (a rolling `0.3.0-0.ciN.` build to the `*-canary` +builds and publishes on every push to `main` (a rolling `0.5.0-0.ciN.g` build to the `*-canary` groups) and on `vX.Y.Z` tags (a clean `X.Y.Z-1` to the base groups, plus attached to the unified Gitea Release) — separate repos, so a stable box never jumps to a canary build (see [Release Channels](https://punktfunk.unom.io/docs/channels)). The `baseurl` below subscribes to the @@ -107,8 +107,9 @@ tracking: `rpm-ostree override` / `rpm-ostree uninstall punktfunk`. ```sh PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh # host + client -PF_VERSION=0.0.1 PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh # + the noarch punktfunk-web (needs bun on PATH) -# -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm (+ punktfunk-web-0.0.1-1.fcNN.noarch.rpm with PF_WITH_WEB=1) +PF_VERSION=0.0.1 PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh # + punktfunk-web (needs bun on PATH) +# -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm (+ punktfunk-web-0.0.1-1.fcNN.x86_64.rpm with PF_WITH_WEB=1; +# the web subpackage vendors a bun binary, so it's arch-specific, not noarch) ``` Run it inside the Fedora 43 builder image so the deps resolve and match Bazzite: @@ -119,4 +120,5 @@ docker run --rm -v "$PWD:/src" -w /src punktfunk-fedora-rpm \ bash -lc 'git config --global --add safe.directory /src && PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh' ``` -A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.0.1-1`. +A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.3.0-1` (the +spec defaults). diff --git a/packaging/windows/README.md b/packaging/windows/README.md index 58fa0f4..94de0fc 100644 --- a/packaging/windows/README.md +++ b/packaging/windows/README.md @@ -108,7 +108,8 @@ read it from `%ProgramData%\punktfunk\web-password`. ## Dev iteration on the test box (driver) Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host -service. Run **elevated**; both default to the `PunktfunkHost` service. +service. Run **elevated**; both default to the `PunktfunkHost` service. (The `C:\t-goal1\...` probe +path below is the maintainer's test box — substitute your own `punktfunk-probe.exe` build.) ```powershell # Recover a WEDGED driver. Symptom: every session fails with diff --git a/packaging/windows/build-gamepad-drivers.ps1 b/packaging/windows/build-gamepad-drivers.ps1 index a7e0020..e7bab28 100644 --- a/packaging/windows/build-gamepad-drivers.ps1 +++ b/packaging/windows/build-gamepad-drivers.ps1 @@ -10,8 +10,8 @@ `cargo build --release` builds the whole workspace (this shares wdk-sys/wdk-build + the bindgen pin with pf-vdisplay). Then, per driver: CLEAR the FORCE_INTEGRITY PE bit, sign the .dll, stampinf a DriverVer into the INF; then Inf2Cat both catalogs and sign them. Both drivers share ONE self-signed cert (or a - supplied DRIVER_CERT secret) + ONE exported .cer - the layout install-gamepad-drivers.ps1 consumes - (per-driver .inf/.cat/.dll + one shared punktfunk-driver.cer). + supplied DRIVER_CERT secret) + ONE exported .cer - the layout `punktfunk-host.exe driver install + --gamepad` consumes (per-driver .inf/.cat/.dll + one shared punktfunk-driver.cer). Output (-Out): pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + punktfunk-driver.cer. diff --git a/packaging/windows/drivers/pf-dualsense/README.md b/packaging/windows/drivers/pf-dualsense/README.md index dcc8fdb..67197aa 100644 --- a/packaging/windows/drivers/pf-dualsense/README.md +++ b/packaging/windows/drivers/pf-dualsense/README.md @@ -8,7 +8,7 @@ DualSense a near-native feel with **no external gamepad dependencies** (no ViGEm Shipping: the driver is one member of the in-tree driver workspace ([`packaging/windows/drivers/`](../../README.md)), built from source in CI, and bundled + `pnputil`-installed by the Windows host [installer](../../README.md). The host feeds it over a shared -memory channel from `crates/punktfunk-host/src/inject/dualsense_windows.rs`. The same UMDF driver also +memory channel from `crates/punktfunk-host/src/inject/windows/dualsense_windows.rs`. The same UMDF driver also serves the **DualShock 4** identity per a `device_type` byte the host stamps. This README captures the driver-authoring lore — the bugs and the signing recipe that make a @@ -37,7 +37,9 @@ $env:LIBCLANG_PATH = 'C:\Program Files\LLVM\bin' $env:Version_Number = '10.0.26100.0' # else wdk-build picks 10.0.28000.0 (no km/crt) and bindgen fails ``` -Then, in the example dir: +The shipping flow is `build-gamepad-drivers.ps1` (one level up): workspace `cargo build --release` +plus the sign steps below, staged for the installer. The original manual dev-box recipe, kept as +lore (paths reflect that era's cargo-make layout): ```powershell cargo make # -> target\debug\pf_dualsense_package\ (.inf/.cat/.dll) @@ -45,7 +47,8 @@ cargo make # -> target\debug\pf_dualsense_package\ # *** CRITICAL: clear the PE FORCE_INTEGRITY bit *** # windows-drivers-rs links the DLL with /INTEGRITYCHECK, which forces a CI-trusted page-hash # signature a self-signed cert cannot satisfy (CodeIntegrity 3004 "hash not found" / -# 3089 VerificationError 7). SudoVDA.dll has this bit OFF. Clear bit 0x80 at PE-header offset +0x5e: +# 3089 VerificationError 7). SudoVDA.dll (third-party VDD prior art, not used by punktfunk) has +# this bit OFF. Clear bit 0x80 at PE-header offset +0x5e: $f = 'target\debug\pf_dualsense_package\pf_dualsense.dll' $b = [IO.File]::ReadAllBytes($f); $pe = [BitConverter]::ToInt32($b,0x3c); $off = $pe + 0x5e $dc = [BitConverter]::ToUInt16($b,$off); $bb = [BitConverter]::GetBytes([uint16]($dc -band 0xFF7F)) @@ -60,9 +63,10 @@ pnputil /add-driver target\debug\pf_dualsense_package\pf_dualsense.inf /install devgen /add /hardwareid "root\pf_dualsense" # creates the (transient, SWD) device node ``` -`devgen` is at `...\Windows Kits\10\Tools\10.0.26100.0\x64\devgen.exe`. SWD devgen devices clear on -reboot (recreate after each boot). TODO: drop the post-build PE patch by stopping wdk-build emitting -`/INTEGRITYCHECK`. +`devgen` (under `Windows Kits\10\Tools\\x64\`) is only for manual testing — the shipping +install is `punktfunk-host.exe driver install --gamepad`, and the host SwDeviceCreate's the device +per session (no persistent devnode). SWD devgen devices clear on reboot. TODO: drop the post-build +PE patch by stopping wdk-build emitting `/INTEGRITYCHECK`. ## The three bugs that made it work (porting a WDK C sample to Rust) diff --git a/packaging/windows/drivers/pf-xusb/README.md b/packaging/windows/drivers/pf-xusb/README.md index 62516ea..1124258 100644 --- a/packaging/windows/drivers/pf-xusb/README.md +++ b/packaging/windows/drivers/pf-xusb/README.md @@ -43,19 +43,21 @@ Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payl @8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) · `large @28` · `small @29`. -## Validated live on `.173` (2026-06-22) +## Validated live (2026-06-22, maintainer's RTX test box) `XInputGetState(0)` returns **CONNECTED** with the pushed buttons/sticks and an incrementing `dwPacketNumber`; `XInputSetState(0xC000, 0x4000)` reaches the driver as `00 00 c0 40 02` → host sees -`large=192 small=64`. Test tools: `C:\Users\Public\giprobe\xusbtest.exe` (creates the `pf_xusb` +`large=192 small=64`. Test tools (on that box): `xusbtest.exe` (creates the `pf_xusb` devnode + cycling state via shm) and `xinputtest.exe` (`XInputGetState`/`SetState` harness). ## Build / sign / install (same recipe as the DualSense driver) -Built from `C:\Users\Public\m0\windows-drivers-rs\examples\pf-xusb` (the `../../crates` paths resolve -there); these repo files are the canonical copies — keep them in sync. +Built as a member of the in-tree [`packaging/windows/drivers/`](../) workspace — one +`cargo build --release` builds all three drivers; `build-gamepad-drivers.ps1` (one level up) wraps +the whole build/sign/stage flow in CI. The manual steps: -1. `cargo make` (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) → `target\debug\pf_xusb_package\`. +1. `cargo build --release` in the workspace (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) → + `target\x86_64-pc-windows-msvc\release\pf_xusb.dll`. 2. Clear the FORCE_INTEGRITY PE bit (bit `0x80` at `e_lfanew+0x5e` of `pf_xusb.dll`). 3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll`. 4. `Inf2Cat /driver: /os:10_X64` → re-sign `pf_xusb.cat` with the same thumbprint. @@ -63,11 +65,12 @@ there); these repo files are the canonical copies — keep them in sync. ## Host integration (done) -`crates/punktfunk-host/src/inject/gamepad_windows.rs` is the Windows `GamepadManager` (used by +`crates/punktfunk-host/src/inject/windows/gamepad_windows.rs` is the Windows `GamepadManager` (used by `PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-`, writes the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There -is **no ViGEmBus dependency** anymore. The driver is built from source (`packaging/windows/drivers/pf-xusb`), -signed, and pnputil-installed by the Inno Setup installer (via `install-gamepad-drivers.ps1`). +is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI +(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via +`punktfunk-host.exe driver install --gamepad`. ## Multi-pad diff --git a/packaging/windows/punktfunk-host.iss b/packaging/windows/punktfunk-host.iss index 12c29b4..0f2b2a0 100644 --- a/packaging/windows/punktfunk-host.iss +++ b/packaging/windows/punktfunk-host.iss @@ -285,8 +285,9 @@ end; function WebSetupParams(Param: String): String; begin - { Pass the password to web-setup.ps1 via a temp file, not the cmdline (which lands in the install - log). Only on a fresh install - on upgrade web-setup keeps the existing file. } + { Pass the password to `punktfunk-host.exe web setup` via a temp file, not the cmdline (which + lands in the install log). Only on a fresh install - on upgrade web setup keeps the existing + file. } Result := '--app-dir "' + ExpandConstant('{app}') + '"'; if FreshWebInstall then Result := Result + ' --password-file "' + ExpandConstant('{tmp}\webpw.txt') + '"'; @@ -312,7 +313,7 @@ end; #ifdef WithWeb { Stop a running web console + free :3000 BEFORE the file copy, so the old server doesn't lock .output / web-run.cmd / bun.exe and the new task can bind. Killing the :3000 listener owner is - runtime-agnostic (an early install may have run node, the current one runs bun). web-setup.ps1 + runtime-agnostic (an early install may have run node, the current one runs bun). `web setup` repeats this idempotently after the copy. Best-effort; a fresh install is a no-op. } procedure StopWebConsole; var @@ -334,7 +335,7 @@ begin StopHostServiceAndWait; #ifdef WithWeb StopWebConsole; { upgrade-safe: free :3000 + unlock the web files before the copy } - { Stash the chosen password for web-setup.ps1 (fresh install only); the temp copy is auto-cleaned. } + { Stash the chosen password for `web setup` (fresh install only); the temp copy is auto-cleaned. } if FreshWebInstall then SaveStringToFile(ExpandConstant('{tmp}\webpw.txt'), Trim(WebPwPage.Values[0]), False); #endif diff --git a/scripts/steamdeck/README.md b/scripts/steamdeck/README.md index 5399b4c..37c1486 100644 --- a/scripts/steamdeck/README.md +++ b/scripts/steamdeck/README.md @@ -39,9 +39,13 @@ git clone https://git.unom.io/unom/punktfunk ~/punktfunk bash ~/punktfunk/scripts/steamdeck/install.sh # PIN pairing required (secure default) bash ~/punktfunk/scripts/steamdeck/install.sh --open # trusted LAN: accept unpaired clients bash ~/punktfunk/scripts/steamdeck/install.sh --no-web # host only, no web console +bash ~/punktfunk/scripts/steamdeck/install.sh --no-gamestream # native punktfunk/1 only, no Moonlight surface bash ~/punktfunk/scripts/steamdeck/update.sh # after pulling new source ``` +Note: unlike a bare `serve` (native-only by default), the Deck install enables `--gamestream` by +default so stock Moonlight clients work out of the box; `--no-gamestream` turns that surface off. + Env overrides: `PUNKTFUNK_SRC` (source dir, default `~/punktfunk`), `PUNKTFUNK_BOX` (container name, default `pf2`), `PUNKTFUNK_MGMT_PORT` (47990), `PUNKTFUNK_WEB_PORT` (3000). diff --git a/scripts/windows/README.md b/scripts/windows/README.md index 9d25044..51530c8 100644 --- a/scripts/windows/README.md +++ b/scripts/windows/README.md @@ -36,7 +36,7 @@ won't start. The service is down only for the build duration. On an **installed** host (the `setup.exe`) the console is set up automatically — no manual steps. The installer bundles the built (self-contained, no-`node_modules`) `.output` server + a portable -bun and runs `scripts\windows\web-setup.ps1`, which registers the **`PunktfunkWeb`** scheduled task +bun and runs `punktfunk-host.exe web setup`, which registers the **`PunktfunkWeb`** scheduled task (at boot, as SYSTEM, restart-on-failure) running `{app}\web\web-run.cmd` → `bun …\.output\server\index.mjs` on `:3000`, opens inbound TCP 3000, and writes the login password to `%ProgramData%\punktfunk\web-password` (ACL'd to Administrators + SYSTEM). The mgmt bearer token it @@ -52,8 +52,8 @@ powershell -ExecutionPolicy Bypass -File scripts\windows\build-web.ps1 `bun install && bun run build` (Nitro `noExternals` -> a self-contained `.output`, no `node_modules`/`.npmrc`), then restarts the `PunktfunkWeb` task and checks `:3000/login`. Use -this to iterate on the console against an installed host - `web-setup.ps1` (or a fresh install) is -what creates the task in the first place. +this to iterate on the console against an installed host - `punktfunk-host.exe web setup` (or a +fresh install) is what creates the task in the first place. ## Typical flow after pulling new code