Files
punktfunk/crates/pf-client-core/src/lib.rs
T
enricobuehlerandClaude Fable 5 ed3d236ab8 feat(pad-audio): DualSense audio haptics + speaker, host->client end to end
The 0xD1 pad-audio plane streams a DualSense's voice-coil haptics (back
channel pair, 5 ms Opus frames) and speaker (front pair, 10 ms) per pad from
a Windows host to the SDL clients, which render them into a USB DualSense's
own 4-channel audio device.

Wire (punktfunk-core, ABI v15): PAD_AUDIO_MAGIC 0xD1 [pad][kind][seq][pts]
[opus]; CLIENT_CAP_PAD_AUDIO 0x04 / HOST_CAP_PAD_AUDIO 0x20; per-pad render
capability rides GamepadArrival flags bits 8/9, sent only toward a host that
advertised its cap so old hosts see byte-identical arrivals; silence is a
frozen seq (mic-mute discipline), loss is a seq gap concealed via
AudioGapTracker. HidOutput::AudioCtl (0xCD kind 0x06) forwards the 0x02
report's audio-control bytes 5..=10 change-only, value-deduped, with a
once-per-pad "title asserted haptics-select" diagnosis log.

Windows host endpoint provider (audio/windows/pad_endpoint.rs): per-pad
render endpoints are additional devnode instances of Valve's Steam Streaming
Speakers driver (SetupDiRegisterDeviceInfo, NOT the class installer - it
needs an interactive window station), stamped with DualSense identity: desc
"Wireless Controller", device name "DualSense Wireless Controller",
ContainerId = the virtual pad's PFDS GUID, 4ch/48k format triplet.
IPropertyStore route first, ACL-repaired registry fallback (the MMDevices
keys deny writes even to SYSTEM; the owner's implicit WRITE_DAC + an ACE for
S-1-5-18 resolved by SID is the way in). Provisioned at host startup
(PUNKTFUNK_PAD_AUDIO, PUNKTFUNK_PAD_AUDIO_SLOTS, default 1), idempotent via
a persisted PunktfunkPadIndex marker; pad endpoints are structurally
ineligible for the mic/loopback wiring plan and guarded against default-
device theft; capture is WASAPI loopback on the stamped endpoint. Devtest:
punktfunk-host pad-endpoint ensure|remove|status.

Host service (native/pad_audio.rs): per-(session,pad) thread, loopback 4ch
-> pair splitter -> per-kind stereo Opus (48k LowDelay CBR 64k) -> per-kind
silence gate (opens at peak>=1e-3, 250 ms hangover, gated = no send + frozen
seq) -> datagrams. Spawned from the native input pump when a DualSense/Edge
arrival carries audio bits and both caps negotiated; idempotent re-arrivals;
reaped on remove and teardown.

Client tier A (pf-client-core/pad_audio.rs): settings pad_haptics (default
on) and pad_speaker (default "pad"); tier A = wired USB DS5/Edge via SDL
connection state with an audio-sibling fallback; correlation maps the SDL
HID path to the pad's own render endpoint (Windows: ContainerId match +
4ch gate via registry; Linux: Sony sink signature); renderer decodes both
kinds into a quad interleave and plays it on the pad's endpoint (WASAPI
autoconvert / PipeWire target.object, 240-2400 frame ring floor,
dont-reconnect so an unplug never re-routes haptics to the desktop
speakers). SDL's DualSense driver sets "disable audio haptics" whenever it
drives rumble emulation, so tier-A pads suppress wire rumble and send one
cleared-enable-bits effects packet to keep the actuators live; AudioCtl
bytes fold back into the effects packet at report-minus-one offsets.

Verification: punktfunk-core 265 tests (macOS) + clippy -D warnings (mac +
Linux docker); pf-inject 85 tests (Linux docker); punktfunk-host cargo
check + clippy + 19 pad tests + 46 audio-module tests (Windows box);
pf-client-core 30 tests + clippy (Linux docker CI image) + cargo check
(Windows box); punktfunk-client-session clippy (Linux) + check (Windows);
cargo fmt --all --check clean on the final tree. NOT yet verified: any
on-glass run (host deploy + real title + physical pad), the stamp-route
split at runtime, exclusive-mode Initialize isolation, Linux-host emission
(the per-pad PipeWire sink is not in this change - Windows hosts only).
Scope excluded deliberately: tier B (Apple CoreHaptics) and tier C
(haptics->rumble derivation), pad_speaker="mix", Android leg, settings UI
surfaces (keys are serde-defaulted), GameStream-plane arrivals (audio_caps
always 0 there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 12:07:06 +02:00

96 lines
4.6 KiB
Rust

//! Shared, UI-agnostic client plumbing, extracted verbatim from the GTK client
//! (design: punktfunk-planning `linux-client-rearchitecture.md`, Phase 0) so the desktop
//! shells and the Vulkan session binary build on one implementation — on Linux AND
//! Windows (the session binary runs on both; macOS stays `wol`-only, clients/apple is
//! the client there).
//!
//! Nothing here may depend on a UI toolkit: the presenter contract is `session`'s
//! channels (`SessionHandle`) and `video`'s `DecodedImage` (RGBA bytes, dmabuf fds +
//! plane layout, or a decoded VkImage) — how frames reach the screen is the consumer's
//! business.
//!
//! Audio is the one per-OS module swap: `audio.rs` (PipeWire) on Linux,
//! `audio_wasapi.rs` (WASAPI) on Windows — same public surface, picked here by `#[path]`
//! so `crate::audio` is the only name the session pump ever sees. `keymap` (evdev-keyed)
//! stays Linux: the session path uses pf-presenter's SDL-scancode table instead.
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in this crate carries a `// SAFETY:`
// proof of why it is sound. This crate held ~91 unsafe items with NO enforcement while every
// other subsystem crate denied it — the decoders' `unsafe impl Send`s had a one-line aside
// instead of an argument precisely because nothing required one.
#![deny(clippy::undocumented_unsafe_blocks)]
#[cfg(target_os = "linux")]
pub mod audio;
#[cfg(windows)]
#[path = "audio_wasapi.rs"]
pub mod audio;
#[cfg(any(target_os = "linux", windows))]
pub mod discovery;
#[cfg(any(target_os = "linux", windows))]
pub mod gamepad;
#[cfg(target_os = "linux")]
pub mod keymap;
#[cfg(any(target_os = "linux", windows))]
pub mod library;
// The `punktfunk://` grammar (design/client-deep-links.md §2): one parser/emitter for the
// shells, the session and the CLI, held to the Swift/Kotlin ports by a shared vector file.
#[cfg(any(target_os = "linux", windows))]
pub mod deeplink;
// The brain layer (design/client-architecture-split.md §3): what a connect is, the wake
// state machine every front-end drives, and the session spawn + stdout contract.
#[cfg(any(target_os = "linux", windows))]
pub mod orchestrate;
// The host's OS-identity chain (mDNS `os=` TXT): sanitize + icon-walk order. Pure string
// logic, built everywhere (the Apple/Android ports mirror it rather than link it).
pub mod os;
// Client settings profiles: the override catalog + the one connect-time resolver
// (design/client-settings-profiles.md §4). Sits beside `trust`, which owns the host records
// the bindings live on.
// Pad audio (the 0xD1 plane): DualSense voice-coil haptics + speaker rendered on the wired
// physical pad's own 4-ch audio device — correlation, the per-session renderer worker, and
// the tier-A pad registry the gamepad worker feeds it through.
#[cfg(any(target_os = "linux", windows))]
pub mod pad_audio;
#[cfg(any(target_os = "linux", windows))]
pub mod profiles;
#[cfg(any(target_os = "linux", windows))]
pub mod session;
#[cfg(any(target_os = "linux", windows))]
pub mod trust;
// "Is a newer client available, and can this box install it?" — the client half of the
// signed-manifest update check the host already runs (design: host-update-from-web-console.md).
// Linux only: the Windows client ships inside the host installer and the Mac one through
// clients/apple, so neither has a package to reason about here.
#[cfg(target_os = "linux")]
pub mod update;
#[cfg(any(target_os = "linux", windows))]
pub mod video;
#[cfg(any(target_os = "linux", windows))]
mod video_color;
#[cfg(any(target_os = "linux", windows))]
mod video_software;
// libav ownership helpers shared by the hardware decoders below (`AvBuffer`).
#[cfg(any(target_os = "linux", windows))]
mod video_libav;
#[cfg(target_os = "linux")]
mod video_vaapi;
#[cfg(any(target_os = "linux", windows))]
mod video_vulkan;
// The OS-clipboard bridge for the shared clipboard (design/clipboard-and-file-transfer.md §5).
// Built everywhere the session client is; the platform seam inside is Windows-real,
// stub elsewhere.
#[cfg(any(target_os = "linux", windows))]
pub mod clipboard;
// PyroWave decode — Linux + Windows (plan §4.5; the Apple Metal port is its own phase).
// Windows joined once its client moved to the SAME spawned Vulkan session presenter as
// Linux's: the decoder is plain Vulkan compute on the presenter's device (no fds, no
// dmabuf, no D3D11 interop), so the old "Windows present-path decision" that gated it
// resolved itself — the present path is now literally the same code.
#[cfg(windows)]
pub mod video_d3d11;
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
pub mod video_pyrowave;
pub mod wol;