feat(protocol,clients): codec preference negotiation + Linux client decodes per Welcome (Phase 2a)
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)
**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
`video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
(drift-guarded against the wire values). Header regenerated.
**Host:** passes `hello.preferred_codec` into `resolve_codec`.
**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.
**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).
Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
`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
|
preference byte (same trailing-byte back-compat pattern as the compositor), the Welcome
|
||||||
echoes the resolved backend — precedence: explicit client choice > `PUNKTFUNK_GAMEPAD`
|
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
|
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
|
capability; impulse-trigger rumble is unreachable through a virtual pad), and the UHID
|
||||||
`hid-playstation` pads — **DualSense** (adaptive triggers, lightbar, touchpad, motion) and
|
`hid-playstation` pads — **DualSense** (adaptive triggers, lightbar, touchpad, motion) and
|
||||||
**DualShock 4** (lightbar, touchpad, motion, rumble; DualSense minus adaptive triggers / player
|
**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
|
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
|
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
|
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
|
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
|
(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
|
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`
|
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
|
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
|
folds to this 360 path). All three UMDF drivers (DualSense/DS4 + XUSB) are built from source in CI
|
||||||
by the Inno Setup installer (`packaging/windows/gamepad-drivers/` + `install-gamepad-drivers.ps1`).
|
(`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`),
|
**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-<index>`, and
|
the driver reads it (`WdfDeviceAllocAndQueryProperty`) to map its own `*-shm-<index>`, and
|
||||||
`UmdfHostProcessSharing=ProcessSharingDisabled` gives each pad its own host (per-pad statics) —
|
`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
|
validated live with 2 distinct XInput slots + 2 DualSense pads. (Client-side multi-pad forwarding is
|
||||||
the remaining piece.)
|
the remaining piece.)
|
||||||
- **Windows host: implemented and shipping (all-vendor, x64-only).** `#[cfg(windows)]` backends
|
- **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**
|
behind the same traits as Linux — **IDD-push capture** straight into the in-house all-Rust IddCx
|
||||||
virtual display per session (`vdisplay/sudovda.rs`), GPU encode (NVENC `--features nvenc`; AMD/Intel
|
**pf-vdisplay** virtual display (`capture/windows/idd_push.rs`, `vdisplay/windows/pf_vdisplay.rs`;
|
||||||
`--features amf-qsv`), SendInput + **ViGEm** gamepads (`inject/gamepad_windows.rs`), WASAPI loopback
|
DXGI Desktop Duplication / WGC as fallbacks, `capture/windows/dxgi.rs`), GPU encode (NVENC
|
||||||
+ virtual mic (`audio/wasapi_*`). Ships as a **signed Inno Setup installer** that registers a
|
`--features nvenc`; AMD/Intel `--features amf-qsv`), SendInput + the in-house UMDF gamepad drivers
|
||||||
`LocalSystem` SCM service launching into the interactive session for secure-desktop (UAC/lock-screen)
|
(`inject/windows/`), WASAPI loopback + virtual mic (`audio/windows/wasapi_*`). Ships as a **signed
|
||||||
capture (`service.rs`), bundles the SudoVDA driver + the FFmpeg DLLs, and is published by
|
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`):
|
`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,
|
`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
|
direct SDK, `encode/windows/nvenc.rs`), **AMF** (AMD) / **QSV** (Intel) via libavcodec
|
||||||
(`encode/ffmpeg_win.rs`, the Windows analogue of the Linux VAAPI backend — `WinVendor{Amf,Qsv}`,
|
(`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-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
|
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
|
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-core/ protocol · FEC · crypto · quic (punktfunk/1 control plane, feature-gated)
|
||||||
crates/punktfunk-host/
|
crates/punktfunk-host/
|
||||||
gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps
|
gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps
|
||||||
vdisplay/{kwin,gamescope,mutter,wlroots}.rs per-compositor client-sized virtual outputs
|
vdisplay/linux/{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)
|
vdisplay/windows/{pf_vdisplay,manager,identity}.rs all-Rust IddCx virtual display (pf-vdisplay)
|
||||||
inject/{libei,wlr,gamepad,dualsense}.rs input backends (uinput xpad + UHID DualSense)
|
linux/zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan)
|
||||||
encode/{nvenc,linux,vaapi,ffmpeg_win,sw}.rs per-GPU encoders (NVENC · Linux NVENC/CUDA · VAAPI · AMF/QSV · openh264)
|
inject/linux/{libei,wlr,gamepad,dualsense,dualshock4,steam_*}.rs Linux input (uinput xpad · UHID pads · virtual Deck)
|
||||||
capture.rs · encode.rs · audio.rs · spike.rs · punktfunk1.rs · mgmt.rs · native_pairing.rs · stats_recorder.rs
|
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/probe/ punktfunk/1 reference/probe client (headless test/measurement tool)
|
||||||
clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3)
|
clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3)
|
||||||
clients/windows/ native Windows client (WinUI 3 via windows-reactor · D3D11 · WASAPI · 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/apple/ native macOS/iOS/tvOS client (Swift · VideoToolbox · GameController)
|
||||||
clients/android/ native Android client (Kotlin app + native/ Rust JNI core over punktfunk-core)
|
clients/android/ native Android client (Kotlin app + native/ Rust JNI core over punktfunk-core)
|
||||||
clients/decky/ Steam Deck Decky plugin
|
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)
|
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)
|
packaging/ apt(deb) · RPM/COPR · Arch/sysext · Flatpak · Bazzite bootc · Windows host installer (per-dir READMEs)
|
||||||
tools/{loss-harness,latency-probe}/ measurement (plan §10)
|
tools/{loss-harness,latency-probe}/ measurement (plan §10)
|
||||||
|
|||||||
@@ -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.
|
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
|
- **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
|
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
|
- **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.
|
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
|
- **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **Android client** (`clients/android`, phone + TV) | ✅ Streaming live: AMediaCodec decode + HDR10, Oboe audio, controllers, discovery, pairing |
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ PUNKTFUNK_AUTOCONNECT=<box-ip> PUNKTFUNK_MODE=1280x720x60 swift run PunktfunkCli
|
|||||||
- **App Store screenshots** are automated — `tools/screenshots.sh all` renders the real UI at the
|
- **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
|
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.
|
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
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -529,9 +529,11 @@ fn speed_test(app: Rc<App>, req: ConnectRequest) {
|
|||||||
},
|
},
|
||||||
CompositorPref::Auto,
|
CompositorPref::Auto,
|
||||||
GamepadPref::Auto,
|
GamepadPref::Auto,
|
||||||
0, // bitrate_kbps (host default)
|
0, // bitrate_kbps (host default)
|
||||||
0, // video_caps: the Linux client has no 10-bit/HDR present path yet
|
0, // video_caps: the Linux client has no 10-bit/HDR present path yet
|
||||||
2, // audio_channels: speed-test probe, stereo
|
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
|
None, // launch: speed-test probe connect, no game
|
||||||
pin,
|
pin,
|
||||||
Some(identity),
|
Some(identity),
|
||||||
@@ -689,6 +691,7 @@ fn start_session_with(app: Rc<App>, req: ConnectRequest, pin: Option<[u8; 32]>,
|
|||||||
bitrate_kbps: s.bitrate_kbps,
|
bitrate_kbps: s.bitrate_kbps,
|
||||||
mic_enabled: s.mic_enabled,
|
mic_enabled: s.mic_enabled,
|
||||||
audio_channels: s.audio_channels,
|
audio_channels: s.audio_channels,
|
||||||
|
preferred_codec: s.preferred_codec(),
|
||||||
pin,
|
pin,
|
||||||
identity: app.identity.clone(),
|
identity: app.identity.clone(),
|
||||||
connect_timeout: opts.connect_timeout,
|
connect_timeout: opts.connect_timeout,
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ pub struct SessionParams {
|
|||||||
pub bitrate_kbps: u32,
|
pub bitrate_kbps: u32,
|
||||||
/// Requested audio channel count (2/6/8); the host echoes the resolved value.
|
/// Requested audio channel count (2/6/8); the host echoes the resolved value.
|
||||||
pub audio_channels: u8,
|
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.
|
/// Stream the default microphone to the host's virtual mic source.
|
||||||
pub mic_enabled: bool,
|
pub mic_enabled: bool,
|
||||||
/// Pinned host fingerprint; `None` = trust on first use (caller persists the observed one).
|
/// Pinned host fingerprint; `None` = trust on first use (caller persists the observed one).
|
||||||
@@ -141,7 +144,9 @@ fn pump(
|
|||||||
params.bitrate_kbps,
|
params.bitrate_kbps,
|
||||||
0, // video_caps: the Linux client has no 10-bit/HDR present path yet
|
0, // video_caps: the Linux client has no 10-bit/HDR present path yet
|
||||||
params.audio_channels,
|
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,
|
params.pin,
|
||||||
Some(params.identity),
|
Some(params.identity),
|
||||||
params.connect_timeout,
|
params.connect_timeout,
|
||||||
@@ -170,7 +175,14 @@ fn pump(
|
|||||||
fingerprint: connector.host_fingerprint,
|
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,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}"))));
|
let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}"))));
|
||||||
|
|||||||
@@ -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
|
/// 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.
|
/// can capture; the resolved count drives the decoder + playback layout.
|
||||||
pub audio_channels: u8,
|
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 {
|
impl Default for Settings {
|
||||||
@@ -149,6 +169,7 @@ impl Default for Settings {
|
|||||||
inhibit_shortcuts: true,
|
inhibit_shortcuts: true,
|
||||||
mic_enabled: false,
|
mic_enabled: false,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
|
codec: "auto".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const RESOLUTIONS: &[(u32, u32)] = &[
|
|||||||
const REFRESH: &[u32] = &[0, 30, 60, 90, 120, 144, 165, 240];
|
const REFRESH: &[u32] = &[0, 30, 60, 90, 120, 144, 165, 240];
|
||||||
const GAMEPADS: &[&str] = &["auto", "xbox360", "dualsense", "xboxone", "dualshock4"];
|
const GAMEPADS: &[&str] = &["auto", "xbox360", "dualsense", "xboxone", "dualshock4"];
|
||||||
const COMPOSITORS: &[&str] = &["auto", "kwin", "wlroots", "mutter", "gamescope"];
|
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.
|
/// punktfunk's own license (MIT OR Apache-2.0), shown on the About dialog's Legal page.
|
||||||
const APP_LICENSE: &str = concat!(
|
const APP_LICENSE: &str = concat!(
|
||||||
@@ -193,6 +196,12 @@ pub fn show(
|
|||||||
]))
|
]))
|
||||||
.build();
|
.build();
|
||||||
audio.add(&surround_row);
|
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()
|
let mic_row = adw::SwitchRow::builder()
|
||||||
.title("Stream microphone")
|
.title("Stream microphone")
|
||||||
.subtitle("Send the default input device to the host's virtual microphone")
|
.subtitle("Send the default input device to the host's virtual microphone")
|
||||||
@@ -242,6 +251,8 @@ pub fn show(
|
|||||||
8 => 2,
|
8 => 2,
|
||||||
_ => 0,
|
_ => 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();
|
let dialog = adw::PreferencesDialog::new();
|
||||||
@@ -263,6 +274,7 @@ pub fn show(
|
|||||||
2 => 8,
|
2 => 8,
|
||||||
_ => 2,
|
_ => 2,
|
||||||
};
|
};
|
||||||
|
s.codec = CODECS[(codec_row.selected() as usize).min(CODECS.len() - 1)].to_string();
|
||||||
s.save();
|
s.save();
|
||||||
});
|
});
|
||||||
dialog.present(Some(parent));
|
dialog.present(Some(parent));
|
||||||
|
|||||||
+44
-12
@@ -76,18 +76,48 @@ enum Backend {
|
|||||||
|
|
||||||
pub struct Decoder {
|
pub struct Decoder {
|
||||||
backend: Backend,
|
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 {
|
impl Decoder {
|
||||||
pub fn new() -> Result<Decoder> {
|
pub fn new(codec_id: ffmpeg::codec::Id) -> Result<Decoder> {
|
||||||
ffmpeg::init().context("ffmpeg init")?;
|
ffmpeg::init().context("ffmpeg init")?;
|
||||||
let choice = std::env::var("PUNKTFUNK_DECODER").unwrap_or_default();
|
let choice = std::env::var("PUNKTFUNK_DECODER").unwrap_or_default();
|
||||||
if choice != "software" {
|
if choice != "software" {
|
||||||
match VaapiDecoder::new() {
|
match VaapiDecoder::new(codec_id) {
|
||||||
Ok(v) => {
|
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 {
|
return Ok(Decoder {
|
||||||
backend: Backend::Vaapi(v),
|
backend: Backend::Vaapi(v),
|
||||||
|
codec_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -99,7 +129,8 @@ impl Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(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)),
|
Ok(f) => Ok(f.map(DecodedFrame::Dmabuf)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, "VAAPI decode failed — falling back to software");
|
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)
|
Ok(None)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,9 +162,9 @@ struct SoftwareDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SoftwareDecoder {
|
impl SoftwareDecoder {
|
||||||
fn new() -> Result<SoftwareDecoder> {
|
fn new(codec_id: ffmpeg::codec::Id) -> Result<SoftwareDecoder> {
|
||||||
let codec =
|
let codec = ffmpeg::decoder::find(codec_id)
|
||||||
ffmpeg::decoder::find(ffmpeg::codec::Id::HEVC).ok_or(anyhow!("no HEVC decoder"))?;
|
.ok_or_else(|| anyhow!("no {codec_id:?} decoder in libavcodec"))?;
|
||||||
let mut ctx = ffmpeg::codec::Context::new_with_codec(codec);
|
let mut ctx = ffmpeg::codec::Context::new_with_codec(codec);
|
||||||
unsafe {
|
unsafe {
|
||||||
let raw = ctx.as_mut_ptr();
|
let raw = ctx.as_mut_ptr();
|
||||||
@@ -142,7 +173,7 @@ impl SoftwareDecoder {
|
|||||||
(*raw).thread_type = ffmpeg::ffi::FF_THREAD_SLICE;
|
(*raw).thread_type = ffmpeg::ffi::FF_THREAD_SLICE;
|
||||||
(*raw).thread_count = 0; // auto
|
(*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 })
|
Ok(SoftwareDecoder { decoder, sws: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +271,7 @@ struct VaapiDecoder {
|
|||||||
unsafe impl Send for VaapiDecoder {}
|
unsafe impl Send for VaapiDecoder {}
|
||||||
|
|
||||||
impl VaapiDecoder {
|
impl VaapiDecoder {
|
||||||
fn new() -> Result<VaapiDecoder> {
|
fn new(codec_id: ffmpeg::codec::Id) -> Result<VaapiDecoder> {
|
||||||
use ffmpeg::ffi;
|
use ffmpeg::ffi;
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut();
|
let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut();
|
||||||
@@ -254,10 +285,11 @@ impl VaapiDecoder {
|
|||||||
if r < 0 {
|
if r < 0 {
|
||||||
bail!("no VAAPI device ({})", ffmpeg::Error::from(r));
|
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() {
|
if codec.is_null() {
|
||||||
ffi::av_buffer_unref(&mut hw_device);
|
ffi::av_buffer_unref(&mut hw_device);
|
||||||
bail!("no HEVC decoder");
|
bail!("no {codec_id:?} decoder");
|
||||||
}
|
}
|
||||||
let ctx = ffi::avcodec_alloc_context3(codec);
|
let ctx = ffi::avcodec_alloc_context3(codec);
|
||||||
(*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device);
|
(*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device);
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ example of driving the protocol end to end: QUIC control plane, UDP data plane,
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
- **Receives a real stream**, writes a playable `.h265`, and reports per-frame
|
- **Receives a real stream**, writes a playable elementary stream (`.h265`/`.h264`/`.av1` — the
|
||||||
**capture→…→reassembled latency** percentiles (the host stamps each frame with its capture clock).
|
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.
|
- **Verification mode** against a synthetic host — byte-checks deterministic test frames.
|
||||||
- **Exercises every plane** with scripted test traffic:
|
- **Exercises every plane** with scripted test traffic:
|
||||||
`--input-test` (mouse/keyboard), `--mic-test` (a 440 Hz Opus tone up to the host mic),
|
`--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
|
- **Discovery** — `--discover [secs]` browses the LAN for `_punktfunk._udp` hosts and prints each
|
||||||
(name, addr:port, pairing requirement, cert fingerprint), then exits.
|
(name, addr:port, pairing requirement, cert fingerprint), then exits.
|
||||||
- **Negotiation knobs** — `--mode WxHxFPS`, `--remode` (mid-stream mode change), `--bitrate`,
|
- **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
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
//! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome:
|
||||||
//!
|
//!
|
||||||
//! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames;
|
//! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames;
|
||||||
//! * **stream** (`frames == 0`, virtual host): receives real NVENC AUs, writes a playable
|
//! * **stream** (`frames == 0`, virtual host): receives real encoded AUs, writes a playable
|
||||||
//! `.h265`, and reports per-frame **capture→…→reassembled latency** percentiles (the host
|
//! elementary stream (the dump extension follows the negotiated codec — `.h265`/`.h264`/`.av1`;
|
||||||
//! stamps each frame with its capture wall clock; same-host runs share that clock).
|
//! 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
|
//! `--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`
|
//! 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
|
//! over mDNS, prints each (name, addr:port, pairing requirement, cert fingerprint to pin), and
|
||||||
//! exits without connecting.
|
//! exits without connecting.
|
||||||
//!
|
//!
|
||||||
//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test]
|
//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--remode WxHxFPS:SECS]
|
||||||
//! [--pin HEX] [--compositor NAME] [--gamepad NAME] | --discover [SECS]`
|
//! [--out FILE] [--bitrate KBPS] [--codec auto|h264|hevc|av1] [--audio-channels 2|6|8]
|
||||||
//! (M4 adds VAAPI decode + wgpu present on this skeleton.)
|
//! [--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 anyhow::{anyhow, Context, Result};
|
||||||
use punktfunk_core::config::GamepadPref;
|
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
|
/// multistream-decodes the host's frames and asserts the per-channel sample count, so it's the
|
||||||
/// headless validator for the surround encode path.
|
/// headless validator for the surround encode path.
|
||||||
audio_channels: u8,
|
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
|
/// `--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.
|
/// id from the host's `GET /api/v1/library`, e.g. `steam:570`). Host resolves it; `None` = none.
|
||||||
launch: Option<String>,
|
launch: Option<String>,
|
||||||
@@ -210,6 +219,12 @@ fn parse_args() -> Args {
|
|||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or(2),
|
.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),
|
launch: get("--launch").map(str::to_string),
|
||||||
speed_test: get("--speed-test").and_then(|s| {
|
speed_test: get("--speed-test").and_then(|s| {
|
||||||
let (kbps, ms) = s.split_once(':')?;
|
let (kbps, ms) = s.split_once(':')?;
|
||||||
@@ -428,6 +443,8 @@ async fn session(args: Args) -> Result<()> {
|
|||||||
video_codecs: punktfunk_core::quic::CODEC_H264
|
video_codecs: punktfunk_core::quic::CODEC_H264
|
||||||
| punktfunk_core::quic::CODEC_HEVC
|
| punktfunk_core::quic::CODEC_HEVC
|
||||||
| punktfunk_core::quic::CODEC_AV1,
|
| 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(),
|
.encode(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
|
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))
|
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:
|
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
|
`no_std` (+ alloc), platform-neutral (GUID/LUID are plain integers each side converts to its own OS
|
||||||
type), and free of `*.workspace = true` inheritance.
|
type), and free of `*.workspace = true` inheritance.
|
||||||
|
|||||||
@@ -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`.)
|
/// [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.)
|
||||||
pub const PUNKTFUNK_VIDEO_CAP_444: u8 = 0x04;
|
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).
|
// Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift).
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
const _: () = {
|
const _: () = {
|
||||||
assert!(PUNKTFUNK_VIDEO_CAP_10BIT == crate::quic::VIDEO_CAP_10BIT);
|
assert!(PUNKTFUNK_VIDEO_CAP_10BIT == crate::quic::VIDEO_CAP_10BIT);
|
||||||
assert!(PUNKTFUNK_VIDEO_CAP_HDR == crate::quic::VIDEO_CAP_HDR);
|
assert!(PUNKTFUNK_VIDEO_CAP_HDR == crate::quic::VIDEO_CAP_HDR);
|
||||||
assert!(PUNKTFUNK_VIDEO_CAP_444 == crate::quic::VIDEO_CAP_444);
|
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).
|
// 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:
|
/// 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
|
/// `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
|
/// 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
|
/// [`punktfunk_connection_audio_channels`]. Advertises HEVC-only with no codec preference (call
|
||||||
/// for that layout. A client that wants surround calls this; everything else inherits stereo.
|
/// [`punktfunk_connect_ex7`] to negotiate the codec).
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Same as [`punktfunk_connect`].
|
/// 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_cert_pem: *const std::os::raw::c_char,
|
||||||
client_key_pem: *const std::os::raw::c_char,
|
client_key_pem: *const std::os::raw::c_char,
|
||||||
timeout_ms: u32,
|
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 {
|
) -> *mut PunktfunkConnection {
|
||||||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||||
if host.is_null() {
|
if host.is_null() {
|
||||||
@@ -1235,6 +1302,8 @@ pub unsafe extern "C" fn punktfunk_connect_ex6(
|
|||||||
bitrate_kbps,
|
bitrate_kbps,
|
||||||
video_caps,
|
video_caps,
|
||||||
crate::audio::normalize_channels(audio_channels),
|
crate::audio::normalize_channels(audio_channels),
|
||||||
|
video_codecs,
|
||||||
|
preferred_codec,
|
||||||
launch,
|
launch,
|
||||||
pin,
|
pin,
|
||||||
identity,
|
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).
|
/// Send one input event to the host as a QUIC datagram (non-blocking enqueue).
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ enum CtrlRequest {
|
|||||||
/// certificate fingerprint, the resolved encoder bitrate (kbps), and the host↔client clock offset
|
/// 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).
|
/// (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`
|
/// 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
|
/// (1 = 4:2:0, 3 = 4:4:4), the resolved audio channel count (2/6/8), and the resolved video codec
|
||||||
/// resolved colour signalling — all from the [`Welcome`].
|
/// (`quic::CODEC_*`), with [`ColorInfo`] the resolved colour signalling — all from the [`Welcome`].
|
||||||
type Negotiated = (
|
type Negotiated = (
|
||||||
Mode,
|
Mode,
|
||||||
CompositorPref,
|
CompositorPref,
|
||||||
@@ -54,6 +54,7 @@ type Negotiated = (
|
|||||||
ColorInfo,
|
ColorInfo,
|
||||||
u8,
|
u8,
|
||||||
u8,
|
u8,
|
||||||
|
u8,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Accumulated state of an in-flight / finished speed test. The data-plane pump mirrors the
|
/// 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
|
/// host that omits it (→ `2`) yields working stereo. The `0xC9` audio frames are encoded with the
|
||||||
/// matching layout.
|
/// matching layout.
|
||||||
pub audio_channels: u8,
|
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.
|
/// 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
|
// 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`].
|
// can capture and echoes the result in [`NativeClient::audio_channels`].
|
||||||
audio_channels: u8,
|
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<String>,
|
launch: Option<String>,
|
||||||
pin: Option<[u8; 32]>,
|
pin: Option<[u8; 32]>,
|
||||||
identity: Option<(String, String)>,
|
identity: Option<(String, String)>,
|
||||||
@@ -316,6 +326,8 @@ impl NativeClient {
|
|||||||
bitrate_kbps,
|
bitrate_kbps,
|
||||||
video_caps,
|
video_caps,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
video_codecs,
|
||||||
|
preferred_codec,
|
||||||
launch,
|
launch,
|
||||||
pin,
|
pin,
|
||||||
identity,
|
identity,
|
||||||
@@ -349,6 +361,7 @@ impl NativeClient {
|
|||||||
color,
|
color,
|
||||||
chroma_format,
|
chroma_format,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
codec,
|
||||||
) = match ready_rx.recv_timeout(timeout) {
|
) = match ready_rx.recv_timeout(timeout) {
|
||||||
Ok(Ok(t)) => t,
|
Ok(Ok(t)) => t,
|
||||||
Ok(Err(e)) => return Err(e),
|
Ok(Err(e)) => return Err(e),
|
||||||
@@ -382,6 +395,7 @@ impl NativeClient {
|
|||||||
color,
|
color,
|
||||||
chroma_format,
|
chroma_format,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
codec,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +703,8 @@ struct WorkerArgs {
|
|||||||
bitrate_kbps: u32,
|
bitrate_kbps: u32,
|
||||||
video_caps: u8,
|
video_caps: u8,
|
||||||
audio_channels: u8,
|
audio_channels: u8,
|
||||||
|
video_codecs: u8,
|
||||||
|
preferred_codec: u8,
|
||||||
launch: Option<String>,
|
launch: Option<String>,
|
||||||
pin: Option<[u8; 32]>,
|
pin: Option<[u8; 32]>,
|
||||||
identity: Option<(String, String)>,
|
identity: Option<(String, String)>,
|
||||||
@@ -721,6 +737,8 @@ async fn worker_main(args: WorkerArgs) {
|
|||||||
bitrate_kbps,
|
bitrate_kbps,
|
||||||
video_caps,
|
video_caps,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
video_codecs,
|
||||||
|
preferred_codec,
|
||||||
launch,
|
launch,
|
||||||
pin,
|
pin,
|
||||||
identity,
|
identity,
|
||||||
@@ -789,10 +807,10 @@ async fn worker_main(args: WorkerArgs) {
|
|||||||
video_caps,
|
video_caps,
|
||||||
// Requested surround channel count; the host echoes the resolved value in Welcome.
|
// Requested surround channel count; the host echoes the resolved value in Welcome.
|
||||||
audio_channels,
|
audio_channels,
|
||||||
// Phase 1: the embeddable clients decode HEVC (their decoders are still HEVC-wired),
|
// The codecs this client can decode + its soft preference (0 = auto). The host
|
||||||
// so advertise HEVC-only until Phase 2 threads real per-client codec caps through the
|
// resolves the emitted codec from these and reports it in `Welcome::codec`.
|
||||||
// connect ABI and switches decoders on `Welcome::codec`.
|
video_codecs,
|
||||||
video_codecs: crate::quic::CODEC_HEVC,
|
preferred_codec,
|
||||||
}
|
}
|
||||||
.encode(),
|
.encode(),
|
||||||
)
|
)
|
||||||
@@ -866,6 +884,7 @@ async fn worker_main(args: WorkerArgs) {
|
|||||||
welcome.color,
|
welcome.color,
|
||||||
welcome.chroma_format,
|
welcome.chroma_format,
|
||||||
welcome.audio_channels,
|
welcome.audio_channels,
|
||||||
|
welcome.codec,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -884,6 +903,7 @@ async fn worker_main(args: WorkerArgs) {
|
|||||||
color,
|
color,
|
||||||
chroma_format,
|
chroma_format,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
codec,
|
||||||
) = match setup.await {
|
) = match setup.await {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -902,6 +922,7 @@ async fn worker_main(args: WorkerArgs) {
|
|||||||
color,
|
color,
|
||||||
chroma_format,
|
chroma_format,
|
||||||
audio_channels,
|
audio_channels,
|
||||||
|
codec,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Input task: embedder events → QUIC datagrams.
|
// Input task: embedder events → QUIC datagrams.
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ pub struct Hello {
|
|||||||
/// clients (decodes to `0`, which [`resolve_codec`] treats as HEVC-only — every pre-negotiation
|
/// clients (decodes to `0`, which [`resolve_codec`] treats as HEVC-only — every pre-negotiation
|
||||||
/// build decoded HEVC).
|
/// build decoded HEVC).
|
||||||
pub video_codecs: u8,
|
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.
|
/// [`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`]
|
/// 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
|
/// 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:
|
/// encoder can produce (`host_capable`, also a bitfield). `preferred` is the client's soft preference
|
||||||
/// **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the compatibility /
|
/// ([`Hello::preferred_codec`], `0` = none): when it's in the shared set it wins; otherwise the tie is
|
||||||
/// software floor). Returns the single-bit codec value, or `None` when the two share nothing — the
|
/// broken by **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the
|
||||||
/// caller then refuses the session with a clear error rather than emitting a stream the client can't
|
/// compatibility / software floor). Returns the single-bit codec value, or `None` when client and host
|
||||||
/// decode.
|
/// share nothing — the caller then refuses the session with a clear error rather than emitting a
|
||||||
pub fn resolve_codec(client_codecs: u8, host_capable: u8) -> Option<u8> {
|
/// stream the client can't decode.
|
||||||
|
pub fn resolve_codec(client_codecs: u8, host_capable: u8, preferred: u8) -> Option<u8> {
|
||||||
// An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent.
|
// An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent.
|
||||||
let client = if client_codecs == 0 {
|
let client = if client_codecs == 0 {
|
||||||
CODEC_HEVC
|
CODEC_HEVC
|
||||||
@@ -133,6 +141,13 @@ pub fn resolve_codec(client_codecs: u8, host_capable: u8) -> Option<u8> {
|
|||||||
client_codecs
|
client_codecs
|
||||||
};
|
};
|
||||||
let shared = client & host_capable;
|
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.
|
// Precedence: HEVC > AV1 > H.264.
|
||||||
[CODEC_HEVC, CODEC_AV1, CODEC_H264]
|
[CODEC_HEVC, CODEC_AV1, CODEC_H264]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -716,8 +731,13 @@ impl Hello {
|
|||||||
// present (video_caps non-zero / audio_channels not stereo) the name/launch length bytes
|
// 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
|
// 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`.
|
// 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 =
|
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) {
|
match (&self.name, &self.launch) {
|
||||||
(None, None) if !need_placeholders => {}
|
(None, None) if !need_placeholders => {}
|
||||||
(name, _) => {
|
(name, _) => {
|
||||||
@@ -734,17 +754,21 @@ impl Hello {
|
|||||||
}
|
}
|
||||||
// video_caps: single trailing byte. Emitted when non-zero OR when a later field follows (so
|
// 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).
|
// 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);
|
b.push(self.video_caps);
|
||||||
}
|
}
|
||||||
// audio_channels: single trailing byte. Emitted when non-stereo OR when video_codecs follows.
|
// audio_channels: emitted when non-stereo OR a later field follows.
|
||||||
if self.audio_channels != 2 || self.video_codecs != 0 {
|
if ac_present || vcodecs_present || pref_present {
|
||||||
b.push(self.audio_channels);
|
b.push(self.audio_channels);
|
||||||
}
|
}
|
||||||
// video_codecs: single trailing byte. Last field; omitted when `0` (older client → HEVC-only).
|
// video_codecs: emitted when non-zero OR preferred_codec follows.
|
||||||
if self.video_codecs != 0 {
|
if vcodecs_present || pref_present {
|
||||||
b.push(self.video_codecs);
|
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
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,6 +849,15 @@ impl Hello {
|
|||||||
let video_caps_off = launch_off + 1 + launch_len;
|
let video_caps_off = launch_off + 1 + launch_len;
|
||||||
b.get(video_caps_off + 2).copied().unwrap_or(0)
|
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]
|
#[test]
|
||||||
fn codec_negotiation_and_back_compat() {
|
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!(
|
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)
|
Some(CODEC_HEVC)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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)
|
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.
|
// 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.
|
// 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!(
|
||||||
assert_eq!(resolve_codec(0, CODEC_H264), None);
|
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 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).
|
// a build that ignores the trailing byte (back-compat: extra bytes are skipped).
|
||||||
@@ -2058,15 +2111,23 @@ mod tests {
|
|||||||
video_caps: 0,
|
video_caps: 0,
|
||||||
audio_channels: 2, // stereo — forces the video_caps/audio_channels placeholders
|
audio_channels: 2, // stereo — forces the video_caps/audio_channels placeholders
|
||||||
video_codecs: CODEC_H264 | CODEC_HEVC,
|
video_codecs: CODEC_H264 | CODEC_HEVC,
|
||||||
|
preferred_codec: CODEC_H264,
|
||||||
};
|
};
|
||||||
let enc = h.encode();
|
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!(
|
assert_eq!(
|
||||||
Hello::decode(&enc).unwrap().video_codecs,
|
Hello::decode(no_pref).unwrap().video_codecs,
|
||||||
CODEC_H264 | CODEC_HEVC
|
CODEC_H264 | CODEC_HEVC
|
||||||
);
|
);
|
||||||
// A pre-codec Hello (no trailing codec byte) decodes to 0 → HEVC-only via resolve_codec.
|
assert_eq!(Hello::decode(no_pref).unwrap().preferred_codec, 0);
|
||||||
let legacy = &enc[..enc.len() - 1]; // drop the codec byte (it was the last field)
|
// 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().video_codecs, 0);
|
||||||
|
assert_eq!(Hello::decode(legacy).unwrap().preferred_codec, 0);
|
||||||
|
|
||||||
// A pre-codec Welcome (no codec byte) decodes to HEVC.
|
// A pre-codec Welcome (no codec byte) decodes to HEVC.
|
||||||
let mut w = Welcome::decode(
|
let mut w = Welcome::decode(
|
||||||
@@ -2145,6 +2206,7 @@ mod tests {
|
|||||||
video_caps: VIDEO_CAP_10BIT,
|
video_caps: VIDEO_CAP_10BIT,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
video_codecs: CODEC_H264 | CODEC_HEVC, // exercise the codec bitfield roundtrip
|
video_codecs: CODEC_H264 | CODEC_HEVC, // exercise the codec bitfield roundtrip
|
||||||
|
preferred_codec: CODEC_HEVC,
|
||||||
};
|
};
|
||||||
assert_eq!(Hello::decode(&h.encode()).unwrap(), h);
|
assert_eq!(Hello::decode(&h.encode()).unwrap(), h);
|
||||||
let s = Start {
|
let s = Start {
|
||||||
@@ -2226,6 +2288,7 @@ mod tests {
|
|||||||
video_caps: 0,
|
video_caps: 0,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
video_codecs: 0,
|
video_codecs: 0,
|
||||||
|
preferred_codec: 0,
|
||||||
};
|
};
|
||||||
let enc = h.encode();
|
let enc = h.encode();
|
||||||
assert_eq!(enc.len(), 26);
|
assert_eq!(enc.len(), 26);
|
||||||
@@ -2332,6 +2395,7 @@ mod tests {
|
|||||||
video_caps: 0,
|
video_caps: 0,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
video_codecs: 0,
|
video_codecs: 0,
|
||||||
|
preferred_codec: 0,
|
||||||
};
|
};
|
||||||
let enc = base.encode();
|
let enc = base.encode();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2381,6 +2445,7 @@ mod tests {
|
|||||||
video_caps: 0,
|
video_caps: 0,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
video_codecs: 0,
|
video_codecs: 0,
|
||||||
|
preferred_codec: 0,
|
||||||
};
|
};
|
||||||
// launch alone (no name): a zero-length name placeholder keeps the offset deterministic.
|
// launch alone (no name): a zero-length name placeholder keeps the offset deterministic.
|
||||||
let with_launch = Hello {
|
let with_launch = Hello {
|
||||||
@@ -2589,6 +2654,7 @@ mod tests {
|
|||||||
video_caps: 0,
|
video_caps: 0,
|
||||||
audio_channels: 2,
|
audio_channels: 2,
|
||||||
video_codecs: 0,
|
video_codecs: 0,
|
||||||
|
preferred_codec: 0,
|
||||||
}
|
}
|
||||||
.encode();
|
.encode();
|
||||||
assert!(PairRequest::decode(&h).is_err(), "abi {abi} parsed as pair");
|
assert!(PairRequest::decode(&h).is_err(), "abi {abi} parsed as pair");
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// 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 host_codecs = crate::encode::Codec::host_wire_caps();
|
||||||
let codec_bit = punktfunk_core::quic::resolve_codec(hello.video_codecs, host_codecs)
|
let codec_bit =
|
||||||
.ok_or_else(|| {
|
punktfunk_core::quic::resolve_codec(hello.video_codecs, host_codecs, hello.preferred_codec)
|
||||||
|
.ok_or_else(|| {
|
||||||
anyhow!(
|
anyhow!(
|
||||||
"no shared video codec: client advertised 0x{:02x}, host can emit 0x{:02x} \
|
"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)",
|
(a software-encode host produces H.264 — the client must advertise CODEC_H264)",
|
||||||
@@ -3976,6 +3977,8 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
0, // video_caps
|
0, // video_caps
|
||||||
2, // audio_channels (stereo)
|
2, // audio_channels (stereo)
|
||||||
|
0, // video_codecs (0 → HEVC-only)
|
||||||
|
0, // preferred_codec (auto)
|
||||||
None, // launch
|
None, // launch
|
||||||
None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client
|
None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client
|
||||||
Some((cert, key)),
|
Some((cert, key)),
|
||||||
@@ -4036,6 +4039,8 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
0, // video_caps
|
0, // video_caps
|
||||||
2, // audio_channels (stereo)
|
2, // audio_channels (stereo)
|
||||||
|
0, // video_codecs
|
||||||
|
0, // preferred_codec
|
||||||
None, // launch
|
None, // launch
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -4062,6 +4067,8 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
0, // video_caps
|
0, // video_caps
|
||||||
2, // audio_channels (stereo)
|
2, // audio_channels (stereo)
|
||||||
|
0, // video_codecs
|
||||||
|
0, // preferred_codec
|
||||||
None, // launch
|
None, // launch
|
||||||
Some(host_fp),
|
Some(host_fp),
|
||||||
Some((cert.clone(), key.clone())),
|
Some((cert.clone(), key.clone())),
|
||||||
|
|||||||
+8
-4
@@ -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 |
|
| [`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 |
|
| [`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.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** |
|
| [`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) |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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) |
|
| [`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 |
|
| [`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) |
|
| [`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/<entryId>/<slot>` mgmt endpoint; refactor `library.rs` into a `library/` dir; 8 open design questions; optional SteamGridDB v2 enrichment. → `game-library-stores`
|
- 6 remaining providers (Desktop/Flatpak, itch.io, Ubisoft Connect, Amazon Games, Battle.net, EA app); the `/library/art/<entryId>/<slot>` mgmt endpoint; refactor `library.rs` into a `library/` dir; 8 open design questions; optional SteamGridDB v2 enrichment. → `game-library-stores`
|
||||||
|
|
||||||
**Controllers / input**
|
**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`
|
- 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**
|
**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**
|
**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`
|
- **#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`
|
||||||
|
|||||||
@@ -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_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_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_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. |
|
| `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,
|
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_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_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_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
|
## Gamepads
|
||||||
|
|
||||||
@@ -151,13 +152,14 @@ good value:
|
|||||||
|
|
||||||
## Multiple devices at once
|
## Multiple devices at once
|
||||||
|
|
||||||
Today the native `punktfunk/1` host (`serve`) streams **one session at a time** — additional clients
|
The native `punktfunk/1` host (`serve`) streams up to **4 sessions at once** by default (an encoder
|
||||||
wait in the accept queue until the active session ends. Each session gets its own virtual display at
|
bound); further clients wait in the accept queue until a slot frees up. Each session gets its own
|
||||||
the client's exact resolution; concurrent native sessions are on the roadmap. (`punktfunk1-host`, the
|
virtual display at the client's exact resolution, sharing the host's input/audio/mic services. The
|
||||||
standalone test host, has a `--max-concurrent N` knob — see the [Host CLI](/docs/host-cli) reference —
|
limit isn't settable from `serve`'s command line yet — `punktfunk1-host`, the standalone test host,
|
||||||
but `serve` does not take that flag.)
|
exposes it as `--max-concurrent N` (see the [Host CLI](/docs/host-cli) reference).
|
||||||
|
|
||||||
## Codec and FEC
|
## 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.
|
- The native protocol adds forward error correction for lossy links — see `PUNKTFUNK_FEC_PCT` above.
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ punktfunk-host punktfunk1-host --source virtual
|
|||||||
| `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
|
| `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
|
||||||
|
|
||||||
`--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`punktfunk1-host`-only** — `serve` does not
|
`--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
|
accept them. On `serve` you arm pairing from the web console instead, and concurrency is fixed at
|
||||||
yet capped from the command line.
|
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
|
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`.
|
hosts from another machine with `punktfunk-probe --discover`.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"forgot-password",
|
"forgot-password",
|
||||||
"---Project---",
|
"---Project---",
|
||||||
"roadmap",
|
"roadmap",
|
||||||
|
"status",
|
||||||
"channels",
|
"channels",
|
||||||
"---Reference---",
|
"---Reference---",
|
||||||
"[API Reference](/api)"
|
"[API Reference](/api)"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ description: What you need to run a punktfunk host — GPU, driver, desktop, and
|
|||||||
|
|
||||||
## Supported setups
|
## 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
|
[Windows host](/docs/windows-host) is also available — see below). These are the Linux desktop
|
||||||
environments it supports today, each with its own guide:
|
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
|
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-<version>` on Ubuntu).
|
setup guide installs the right package (e.g. `libnvidia-gl-<version>` on Ubuntu).
|
||||||
- **`nvidia-drm modeset=1`** must be enabled (Wayland on NVIDIA needs it). The setup guides cover this.
|
- **`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);
|
> 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.
|
> workstation cards don't. This only matters if you stream to many devices simultaneously.
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ see [Status & Progress](/docs/status).
|
|||||||
|
|
||||||
- **Windows client on-glass validation.** The hardware (D3D11VA) decode, HDR present, and GUI are
|
- **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.
|
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` →
|
- **Apple presenter polish.** The lower-latency `VTDecompressionSession` → `CAMetalLayer` stage-2
|
||||||
`CAMetalLayer` path is live behind an opt-in flag and graduating to the default.
|
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.
|
- **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
|
- **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.
|
AMD (AMF) and Intel (QSV) encode paths, which are CI-green but newer than NVENC.
|
||||||
|
|||||||
@@ -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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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**.
|
**gamescope**, **Mutter**, and **Sway/wlroots**.
|
||||||
- **Zero-copy GPU pipeline.** Captured frames stay on the GPU (dmabuf → CUDA → NVENC) with
|
- **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
|
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
|
- **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 —
|
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.
|
HDR there is blocked upstream at the compositor.
|
||||||
@@ -55,7 +55,7 @@ host is newer than the Linux host.)
|
|||||||
|
|
||||||
| Client | Highlights |
|
| 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. |
|
| **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.** |
|
| **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. |
|
| **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)
|
- **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.
|
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
|
- **On-glass validation of the Windows client** (D3D11VA decode, HDR present, GUI) on real
|
||||||
GPU hardware.
|
GPU hardware.
|
||||||
- **gamescope multi-user isolation** — per-session input/audio so concurrent sessions can
|
- **gamescope multi-user isolation** — per-session input/audio so concurrent sessions can
|
||||||
|
|||||||
@@ -96,5 +96,3 @@ The check follows the [channel](/docs/channels) you installed from: a plugin ins
|
|||||||
|
|
||||||
The plugin source lives in
|
The plugin source lives in
|
||||||
[`clients/decky`](https://git.unom.io/unom/punktfunk/src/branch/main/clients/decky/README.md).
|
[`clients/decky`](https://git.unom.io/unom/punktfunk/src/branch/main/clients/decky/README.md).
|
||||||
</content>
|
|
||||||
</invoke>
|
|
||||||
|
|||||||
@@ -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
|
clients' [speed test](/docs/configuration#bitrate) picks a safe value; with Moonlight, set it
|
||||||
manually.
|
manually.
|
||||||
- Prefer a **wired** connection or 5 GHz Wi-Fi between host and client.
|
- 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
|
- Streaming to **many devices at once** shares the GPU encoder. The host serves several
|
||||||
(`serve`) handles one native session at a time, with extra clients queued; heavy load is
|
concurrent native sessions (up to 4 by default); heavy load is usually bitrate-bound, so
|
||||||
usually bitrate-bound, so lower the bitrate first.
|
lower the bitrate first.
|
||||||
|
|
||||||
## Still stuck?
|
## Still stuck?
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,16 @@
|
|||||||
// [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.)
|
// [`punktfunk_connection_chroma_format`] reports the real value. (Mirrors `quic::VIDEO_CAP_444`.)
|
||||||
#define PUNKTFUNK_VIDEO_CAP_444 4
|
#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.
|
// 16-byte AEAD authentication tag appended by GCM.
|
||||||
#define TAG_LEN 16
|
#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:
|
// 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
|
// `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
|
// 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
|
// [`punktfunk_connection_audio_channels`]. Advertises HEVC-only with no codec preference (call
|
||||||
// for that layout. A client that wants surround calls this; everything else inherits stereo.
|
// [`punktfunk_connect_ex7`] to negotiate the codec).
|
||||||
//
|
//
|
||||||
// # Safety
|
// # Safety
|
||||||
// Same as [`punktfunk_connect`].
|
// Same as [`punktfunk_connect`].
|
||||||
@@ -1009,6 +1019,36 @@ PunktfunkConnection *punktfunk_connect_ex6(const char *host,
|
|||||||
uint32_t timeout_ms);
|
uint32_t timeout_ms);
|
||||||
#endif
|
#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)
|
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||||
// Generate a persistent client identity: a self-signed certificate + private key, both
|
// 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
|
// 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);
|
PunktfunkStatus punktfunk_connection_chroma_format(PunktfunkConnection *c, uint8_t *out);
|
||||||
#endif
|
#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)
|
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||||
// Send one input event to the host as a QUIC datagram (non-blocking enqueue).
|
// Send one input event to the host as a QUIC datagram (non-blocking enqueue).
|
||||||
//
|
//
|
||||||
|
|||||||
+10
-4
@@ -1,8 +1,9 @@
|
|||||||
# Packaging punktfunk for Fedora / Bazzite
|
# Packaging punktfunk for Fedora / Bazzite
|
||||||
|
|
||||||
The punktfunk host is Linux-only and links system FFmpeg (NVENC), PipeWire, Opus and
|
The punktfunk host links system FFmpeg (NVENC on NVIDIA, VAAPI on AMD/Intel, with a GPU-less
|
||||||
the NVIDIA driver. This directory packages it for the **Fedora Atomic / Bazzite** world
|
software-H.264 fallback), PipeWire and Opus. This page covers packaging it for the
|
||||||
(rpm-ostree + bootc), where most of those deps are already present.
|
**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
|
> 👉 **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).
|
> [`debian/README.md`](debian/README.md) (`apt update && apt upgrade` for new builds).
|
||||||
@@ -20,6 +21,10 @@ packaging/
|
|||||||
copr/ # COPR build-from-SCM settings
|
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
|
## What's needed beyond base Fedora
|
||||||
|
|
||||||
| Dependency | Where it comes from |
|
| 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)
|
## Building the SRPM/RPM locally (Fedora only)
|
||||||
|
|
||||||
```sh
|
```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
|
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.)
|
(Not buildable on Debian/Ubuntu — use a Fedora toolbox/container or COPR.)
|
||||||
|
|||||||
@@ -404,8 +404,8 @@ Debian/Ubuntu — the host links system FFmpeg/PipeWire and won't build there),
|
|||||||
`packaging/README.md`:
|
`packaging/README.md`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git archive --format=tar.gz --prefix=punktfunk-0.0.1/ \
|
git archive --format=tar.gz --prefix=punktfunk-0.3.0/ \
|
||||||
-o ~/rpmbuild/SOURCES/punktfunk-0.0.1.tar.gz HEAD
|
-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
|
rpmbuild -ba packaging/rpm/punktfunk.spec # needs the spec's BuildRequires + RPM Fusion
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
`punktfunk-host` is published as a `.deb` to **Gitea's Debian package registry** in the public
|
`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
|
`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<sha>` build to the **`canary`** apt
|
and publishes on every push to `main` (a rolling `0.5.0~ciN.g<sha>` build to the **`canary`** apt
|
||||||
distribution) and on `vX.Y.Z` tags (a clean `X.Y.Z` to the **`stable`** distribution, plus attached
|
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 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
|
to a canary build — see [Release Channels](https://punktfunk.unom.io/docs/channels). The repo line
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
`punktfunk-host` is published as an RPM to **Gitea's RPM package registry** in the public `unom`
|
`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
|
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`)
|
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.<sha>` build to the `*-canary`
|
builds and publishes on every push to `main` (a rolling `0.5.0-0.ciN.g<sha>` 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
|
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
|
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
|
[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
|
```sh
|
||||||
PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh # host + client
|
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)
|
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.noarch.rpm with PF_WITH_WEB=1)
|
# -> 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:
|
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'
|
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).
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ read it from `%ProgramData%\punktfunk\web-password`.
|
|||||||
## Dev iteration on the test box (driver)
|
## 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
|
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
|
```powershell
|
||||||
# Recover a WEDGED driver. Symptom: every session fails with
|
# Recover a WEDGED driver. Symptom: every session fails with
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
`cargo build --release` builds the whole workspace (this shares wdk-sys/wdk-build + the bindgen pin with
|
`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
|
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
|
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
|
supplied DRIVER_CERT secret) + ONE exported .cer - the layout `punktfunk-host.exe driver install
|
||||||
(per-driver .inf/.cat/.dll + one shared punktfunk-driver.cer).
|
--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.
|
Output (-Out): pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + punktfunk-driver.cer.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
Shipping: the driver is one member of the in-tree driver workspace
|
||||||
([`packaging/windows/drivers/`](../../README.md)), built from source in CI, and bundled +
|
([`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
|
`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.
|
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
|
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
|
$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
|
```powershell
|
||||||
cargo make # -> target\debug\pf_dualsense_package\ (.inf/.cat/.dll)
|
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 ***
|
# *** CRITICAL: clear the PE FORCE_INTEGRITY bit ***
|
||||||
# windows-drivers-rs links the DLL with /INTEGRITYCHECK, which forces a CI-trusted page-hash
|
# 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" /
|
# 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'
|
$f = 'target\debug\pf_dualsense_package\pf_dualsense.dll'
|
||||||
$b = [IO.File]::ReadAllBytes($f); $pe = [BitConverter]::ToInt32($b,0x3c); $off = $pe + 0x5e
|
$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))
|
$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 /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
|
`devgen` (under `Windows Kits\10\Tools\<ver>\x64\`) is only for manual testing — the shipping
|
||||||
reboot (recreate after each boot). TODO: drop the post-build PE patch by stopping wdk-build emitting
|
install is `punktfunk-host.exe driver install --gamepad`, and the host SwDeviceCreate's the device
|
||||||
`/INTEGRITYCHECK`.
|
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)
|
## The three bugs that made it work (porting a WDK C sample to Rust)
|
||||||
|
|
||||||
|
|||||||
@@ -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) ·
|
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
||||||
`large @28` · `small @29`.
|
`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
|
`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
|
`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).
|
devnode + cycling state via shm) and `xinputtest.exe` (`XInputGetState`/`SetState` harness).
|
||||||
|
|
||||||
## Build / sign / install (same recipe as the DualSense driver)
|
## 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
|
Built as a member of the in-tree [`packaging/windows/drivers/`](../) workspace — one
|
||||||
there); these repo files are the canonical copies — keep them in sync.
|
`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`).
|
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`.
|
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll`.
|
||||||
4. `Inf2Cat /driver:<pkg> /os:10_X64` → re-sign `pf_xusb.cat` with the same thumbprint.
|
4. `Inf2Cat /driver:<pkg> /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)
|
## 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-<index>`, writes
|
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, writes
|
||||||
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
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`),
|
is **no ViGEmBus dependency** anymore. The driver is built + signed from source in CI
|
||||||
signed, and pnputil-installed by the Inno Setup installer (via `install-gamepad-drivers.ps1`).
|
(`build-gamepad-drivers.ps1`) and installed by the Inno Setup installer via
|
||||||
|
`punktfunk-host.exe driver install --gamepad`.
|
||||||
|
|
||||||
## Multi-pad
|
## Multi-pad
|
||||||
|
|
||||||
|
|||||||
@@ -285,8 +285,9 @@ end;
|
|||||||
|
|
||||||
function WebSetupParams(Param: String): String;
|
function WebSetupParams(Param: String): String;
|
||||||
begin
|
begin
|
||||||
{ Pass the password to web-setup.ps1 via a temp file, not the cmdline (which lands in the install
|
{ Pass the password to `punktfunk-host.exe web setup` via a temp file, not the cmdline (which
|
||||||
log). Only on a fresh install - on upgrade web-setup keeps the existing file. }
|
lands in the install log). Only on a fresh install - on upgrade web setup keeps the existing
|
||||||
|
file. }
|
||||||
Result := '--app-dir "' + ExpandConstant('{app}') + '"';
|
Result := '--app-dir "' + ExpandConstant('{app}') + '"';
|
||||||
if FreshWebInstall then
|
if FreshWebInstall then
|
||||||
Result := Result + ' --password-file "' + ExpandConstant('{tmp}\webpw.txt') + '"';
|
Result := Result + ' --password-file "' + ExpandConstant('{tmp}\webpw.txt') + '"';
|
||||||
@@ -312,7 +313,7 @@ end;
|
|||||||
#ifdef WithWeb
|
#ifdef WithWeb
|
||||||
{ Stop a running web console + free :3000 BEFORE the file copy, so the old server doesn't lock
|
{ 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
|
.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. }
|
repeats this idempotently after the copy. Best-effort; a fresh install is a no-op. }
|
||||||
procedure StopWebConsole;
|
procedure StopWebConsole;
|
||||||
var
|
var
|
||||||
@@ -334,7 +335,7 @@ begin
|
|||||||
StopHostServiceAndWait;
|
StopHostServiceAndWait;
|
||||||
#ifdef WithWeb
|
#ifdef WithWeb
|
||||||
StopWebConsole; { upgrade-safe: free :3000 + unlock the web files before the copy }
|
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
|
if FreshWebInstall then
|
||||||
SaveStringToFile(ExpandConstant('{tmp}\webpw.txt'), Trim(WebPwPage.Values[0]), False);
|
SaveStringToFile(ExpandConstant('{tmp}\webpw.txt'), Trim(WebPwPage.Values[0]), False);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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 # PIN pairing required (secure default)
|
||||||
bash ~/punktfunk/scripts/steamdeck/install.sh --open # trusted LAN: accept unpaired clients
|
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-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
|
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,
|
Env overrides: `PUNKTFUNK_SRC` (source dir, default `~/punktfunk`), `PUNKTFUNK_BOX` (container name,
|
||||||
default `pf2`), `PUNKTFUNK_MGMT_PORT` (47990), `PUNKTFUNK_WEB_PORT` (3000).
|
default `pf2`), `PUNKTFUNK_MGMT_PORT` (47990), `PUNKTFUNK_WEB_PORT` (3000).
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
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` →
|
(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
|
`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
|
`%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
|
`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
|
`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
|
this to iterate on the console against an installed host - `punktfunk-host.exe web setup` (or a
|
||||||
what creates the task in the first place.
|
fresh install) is what creates the task in the first place.
|
||||||
|
|
||||||
## Typical flow after pulling new code
|
## Typical flow after pulling new code
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user