PyroWaveEncoder behind --features pyrowave + an explicit
PUNKTFUNK_ENCODER=pyrowave (loud EXPERIMENTAL warning: no client can
decode the stream until CODEC_PYROWAVE negotiation lands, plan Phase 2).
Design (plan §4.3): a private ash Vulkan-1.3 device shared with pyrowave
via pyrowave_create_device — DeviceHold pins the instance/device
create-infos the 0.4.0 API requires alive for the device's lifetime.
Capture dmabufs pass straight through on ANY vendor
(linux_zero_copy_is_vaapi → true for pyrowave; NVIDIA dmabuf→Vulkan
import validated by upstream's interop test on .21) with the same
per-buffer import cache as the Vulkan Video backend; the shared
rgb2yuv.comp BT.709-limited CSC writes R8+RG8 images pyrowave samples
directly (R/G view swizzles synthesize Cb/Cr — no NV12 copy). Encode
records into OUR command buffer (pyrowave_device_set_command_buffer), so
ingest + CSC + encode are one submission with a sub-ms fence wait; the
AU is exactly one pyrowave packet, keyframe=true on every frame.
reconfigure_bitrate is a free in-place budget change (Phase 3 pins the
session rate); reset() recreates only the pyrowave encoder object.
Shared ash leaf helpers (dmabuf import, image/memory utils) extracted
from vulkan_video.rs into encode/linux/vk_util.rs — vulkan-encode
builds unchanged.
Validated on .21 (RTX 5070 Ti): pyrowave_smoke green — encodes CPU
fills through the full open→CSC→GPU-encode→packetize path, decodes
every AU with upstream's own decoder, checks BT.709 plane means ±3;
rate retarget + rebuild covered. clippy clean, 308 host tests green
with the feature on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0 of design/pyrowave-codec-plan.md — the opt-in wired-LAN ultra-low-
latency codec. Vendored at upstream 509e4f88 (API 0.4.0, Granite 44362775,
volk + vulkan-headers pins in PUNKTFUNK-VENDOR.txt), pruned to the 6.6 MB
the standalone no-renderer build needs; scripts/vendor-pyrowave.sh
reproduces the tree (a pin bump is protocol-affecting, plan §4.2).
build.rs drives the wrapper CMakeLists (static archives incl. a static
C-API lib upstream only ships shared) + bindgen over pyrowave.h; Linux and
Windows only, empty stub elsewhere (Apple gets a native Metal port, §4.7).
Offline-safe by construction: no network, no system lib, vendored Vulkan
headers — same model as the opus dep (flatpak builder has no network).
Phase-0 validation on .21 (RTX 5070 Ti, driver 610.43.03):
- upstream pyrowave-c-test + interop test (incl. dmabuf/DRM-modifier
Vulkan<->Vulkan) pass, from the pristine AND the pruned tree
- GPU kernel times at ~1.6 bpp noise: encode/decode 0.090/0.042 ms @800p,
0.146/0.067 @1080p, 0.226/0.103 @1440p, 0.477/0.201 @4K — order of
magnitude under NVENC's 1-2 ms retrieve, CBR lands within ~100 B of
target
- cargo test -p pyrowave-sys green (static link + API-version pin check)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ITSAppUsesNonExemptEncryption = true — the app's AES-GCM session crypto is
non-exempt under the App Store Connect encryption questionnaire (category
chosen; French ANSSI declaration in progress). First of the six targets;
the remaining Info.plists follow with the rest of the compliance work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nvenc feature is off by default, and a Linux host built without
--features punktfunk-host/nvenc silently compiles the direct-SDK path out:
a CUDA session degrades to libav hevc_nvenc — no RFI loss recovery, an
encoder rebuild + IDR on every adaptive-bitrate step, and the libav bitrate
clamp — with nothing in the logs saying why. This bit the Linux packagers
once (fixed in e89b2f60) and an ad-hoc host deploy again on 2026-07-14,
where the on-glass Automatic-climb session showed rebuild-per-step behavior
that read as a pipeline gap (it wasn't: the Portal/PipeWire path delivers
EGL-imported CUDA NV12 frames and goes direct whenever the feature is in
the build). One WARN per process, skipped under an explicit
PUNKTFUNK_NVENC_DIRECT=0.
Validated on .21 (GNOME/Mutter Portal capture, feature build): probe session
logs `Linux direct-SDK NVENC`, and probe --rebitrate lands as `encoder
bitrate reconfigured in place (adaptive bitrate — no IDR)`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VAAPI-first on desktop RADV (46b7ffc0) was a regression: Vulkan Video decode
outperforms VAAPI on AMD (on-glass verdict). Vulkan-first is safe there since
the same commit's failure-streak demotion lands on VAAPI, not software — a
broken Mesa Vulkan path still ends up on the working driver.
Auto's order is now: Vulkan first on NVIDIA (no usable VAAPI) + all AMD
(perf; VanGogh additionally chroma-fringes over VAAPI); VAAPI first stays on
Intel/unknown (ANV's Vulkan Video is the least-proven Mesa path). Policy test
updated; 26 pf-client-core tests + clippy green on Linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0.4 host half: PUNKTFUNK_PERF now splits the send thread per window into
fec/seal/sock (SealPerf via Session::take_seal_perf; the paced video path folds
its chunk-send time in through note_sock_ns), logged with per-packet ns in the
send loop's perf line. Measured on .21 at 2.5 Gbps offered: fec ~100 ns/pkt
(Phase 1.4 landed), seal ~1000 ns/pkt = 21.5% of a core, sock ~1400 ns/pkt —
the Phase 1.5 gate (seal > ~15% of the thread at 2 Gbps) trips.
Phase 1.5: seal_frame_inner is now write-then-seal — packetize writes every
packet's plaintext at its final wire offset, then a frame of >= 256 wire
packets (~300 KB) splits the AES-GCM pass across two lanes: a persistent
punktfunk-seal2 worker (lazy-spawned, rendezvous channels, no per-frame spawn,
zero steady-state allocs via a reused hand-off Vec) seals the back half under
nonces seq_base+i while the send thread seals the front. Nonce order is
deterministic per shard index, so the wire is byte-identical to the sequential
pass — pinned by the wire-equivalence test, now including a 469-packet frame
plus an assertion that the lane actually spawned. Small frames and the probe's
~17-packet AUs stay single-lane; PUNKTFUNK_SEAL_LANES=1 forces single-lane.
Validated: 84 core tests + workspace suites + clippy -D warnings on .21.
Halves the seal wall-clock on big frames — headroom for the 10G pair's ~4.8
Gbps ceiling (seal alone would be ~47% of a core there) and PyroWave 4K rates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps [workspace.package] version 0.10.1 -> 0.11.0 (14 workspace crates) and
syncs Cargo.lock (versions-only). Apple MARKETING_VERSION / Android versionName
are set from the release tag by CI, so no client manifest changes; the nested
Windows-driver workspace keeps its independent 0.0.1 version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows counterpart of 5c7e0afa's Linux per-pad pairing MAC: every virtual
DualSense / Edge / DualShock 4 presented ONE hardcoded serial, so SDL/Steam
(which dedup controllers by serial) could merge a second pad into the first.
* GET_FEATURE pairing replies (DS/Edge 0x09, DS4 0x12) now carry the pad
index the host stamps into the sealed section in the MAC's low octet.
* GET_STRING serial strings (HidD_GetSerialNumberString — what SDL actually
reads on Windows) get the same per-pad low octet, agreeing with the
feature MAC. The Edge's 0x09 reply moves onto its serial-string base
(0x75 = DS base + 1), fixing the pre-existing feature-vs-string mismatch.
* The Deck identity already did this per-pad; its two inline index reads
now share the new `pad_index()` helper.
Pad 0 keeps today's serial values for DS / DS4 / Deck (no identity churn
for existing single-pad setups).
Verified on the windows-amd64 runner: cargo build + clippy -D warnings
(pf-umdf-util / pf-xusb / pf-dualsense) + fmt clean on the pinned 1.96.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1.4 (throughput-beyond-1gbps.md): the send path built a fresh erasure
codec and allocated fresh parity Vecs for every FEC block. New trait method
ErasureCoder::encode_into generates parity into caller-pooled buffers; the
packetizer keeps one parity pool that grows once to the session's high-water
recovery count.
- gf16: one cached reed_solomon_simd::ReedSolomonEncoder per coder, re-shaped
per block via reset() (reuses its working space) — the old encode()
convenience call paid engine CPU-feature detection, FFT planning, and
work-buffer allocation per block.
- gf8: last-used (k, m) Cauchy codec cached, so the generator-matrix build
drops out of steady-state frames; parity buffers shaped without re-zeroing
(encode_sep's first-input pass overwrites every row). The GameStream
VideoPacketizer now owns a persistent coder so the cache survives frames.
- encode() delegates to encode_into — one code path, and the nanors byte-exact
parity vector keeps pinning Moonlight wire compatibility.
Validated: 145 core + 308 host tests + clippy -D warnings on .21, loss-harness
recovery curve identical, pipeline bench +0.6-2.4% thrpt (all configs, p<0.05;
the loopback bench is encoder-dominated so the alloc savings mostly land
outside it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-cause fixes for "rumble + adaptive triggers never work with Linux hosts"
(the capture code itself was proven good on-hardware — see the new tests):
* 60-punktfunk.rules now grants the `input` group the VIRTUAL pads' hidraw
nodes (DS/Edge/DS4/Switch/Deck/SC). Steam/SDL drive DualSense adaptive
triggers, lightbar, and player LEDs exclusively over hidraw — and Steam
without hidraw demotes a PlayStation pad to a generic evdev device, losing
its rumble handling too. Coverage no longer depends on the distro's
steam-devices rules + logind's active-seat uaccess ACL (which a headless/
dedicated streaming session never gets). Verified live: nodes now come up
root:input 0660.
* Per-pad MAC in the DualSense (0x09) and DS4 (0x12) pairing feature replies:
hid-playstation adopts the MAC as the HID uniq and SDL/Steam dedup
controllers by that serial — identical MACs made a second virtual pad read
as the first one re-connecting over another transport.
* DualSense/DS4 UHID backends now ack UHID_SET_REPORT (err=0) instead of
ignoring it, so a SET_REPORT writer no longer blocks on the kernel's 5 s
timeout.
* New #[ignore] on-box tests play the GAME's role against a real kernel and
pin the full feedback surface (all green on real hw): DualSense evdev-FF +
raw hidraw output report (rumble/lightbar/LEDs/both trigger blocks verbatim,
per-pad uniq), uinput X-Box FF upload→pump→stop-on-erase, and usbip Deck
0xEB rumble via the controller interface (idle interfaces ACK silently,
like real hardware).
Windows note: the UMDF driver keeps its own pairing blob copies — the shared-
MAC dedup hazard exists there too and needs a driver-side follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1.2: the native plane's pace chunks are rate-adaptive — 16 packets at
today's rates, coarsening until the per-chunk interval clears the 500 µs sleep
floor, capped at 64 (the GSO segment limit). Decouples the syscall batch from
the pace step, so a ≥1 Gbps frame's overflow keeps real sleeps between chunks
(and costs 4× fewer syscalls) instead of collapsing into an unpaced blast.
Phase 1.3: the auto microburst cap scales with the frame — max(128 KB, the
AU's wire bytes / 4) — so high-rate frames burst a bounded quarter and pace
the rest; PUNKTFUNK_PACE_BURST_KB now pins an absolute override.
GameStream plane untouched (its schedule stays pinned by the deterministic
tests, now also asserting budget-independence). Linux GSO latch-off warns
once (was silent; USO already warned).
Linux GSO default stays OPT-IN: the post-1.2/1.3 A/B on the 2.5GbE-hop pair
(.21 → M3 Ultra) reproduced the regression bit-for-bit — 2452 Mbps sendmmsg
vs 1909 GSO peak, 0.4% loss at 1500 where sendmmsg is clean. The super-buffer
trains lose on the constrained hop in the transport path itself (per-AU
probe sends, no video pacer involved), so the block is fabric evidence, not
pacing readiness. Control sweep on this build matched the sendmmsg baseline
exactly (2452); loss-harness recovery curve identical; workspace clippy +
tests green on .21.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reversed/reordered delivery lets a FEC block reconstruct EARLY
(data + recovery >= k), counting still-in-flight shards into
fec_recovered_shards; window_loss_ppm then reported pure reordering as
loss, inflating LossReports — which size adaptive FEC and, since the
Automatic overhaul, feed the ABR controller (one severe window ends slow
start FOR GOOD, so a reorder burst could permanently kneecap a session's
climb).
Early reconstruct stays (it's the latency-right choice); the accounting
now nets it out. The reassembler counts a new fec_late_shards stat when a
parity-restored data shard ARRIVES after all — matched exactly: the
completed/abandoned-frame memory (ReassemblyWindow::completed, now a map)
remembers which shards each terminal frame reconstructed, and a late
arrival must match one (removed on hit), so wire duplicates of delivered
shards and stragglers of failed blocks count nothing. In-flight blocks
dedup via have_data. window_loss_ppm takes the late delta and estimates
from (recovered - late), saturating across window boundaries; both
callers (client core + probe) pass it.
The e2e reorder tests now assert the NET equals the true kill count in
both delivery orders, dup included (previously documented as a known
inflation). Not mirrored into the C-ABI PunktfunkStats — the loss windows
run in-core on every platform.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Windows twin of nvenc_cuda_reconfigure_no_idr, green on the .173 RTX
box (release profile — the dev-profile test binary trips a pre-existing
LNK2019 on the sdk crate's unused safe EncodeAPI statics, which release
LTO strips).
Chasing this also uncovered why the live A/B kept rebuilding: the
PunktfunkHost service runs C:\Users\Public\punktfunk-native's exe, not
the Developer clone deploy-host.ps1 had been rebuilding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gpu-session TrackedEncoder wrapper delegates every Encoder method by
hand, so the new reconfigure_bitrate fell through to the trait's false
default and EVERY bitrate change silently took the rebuild+IDR path — the
live .21 A/B caught it (host log said 'rebuilt', never 'in place').
Also:
- punktfunk-probe --rebitrate KBPS:SECS — headless mid-stream SetBitrate
validator (cursor-wiggles so a damage-driven idle desktop keeps
publishing frames through the switch). Live-verified on .21: one NVENC
session open, then 'encoder bitrate reconfigured in place (adaptive
bitrate — no IDR)' at 20→60 Mbps.
- on-hardware nvenc_cuda reconfigure smoke test (20→60→10 Mbps in place,
zero IDRs — green on the RTX 5070 Ti).
- BitrateChanged doc no longer claims the switch costs an IDR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every adaptive-bitrate step used to tear the encoder down and rebuild it,
opening on a full IDR (a 20-40x frame-size spike, in-flight AU forfeit and
an IDR-cooldown anchor) — exactly when the Automatic controller is climbing.
Encoder::reconfigure_bitrate(bps) retargets the LIVE encoder instead
(default false, so libavcodec/software paths keep the rebuild fallback,
which also still owns the bitrate clamping):
- Linux + Windows direct NVENC: nvEncReconfigureEncoder (added to the
hand-rolled runtime EncodeApi tables) with resetEncoder=0 / forceIDR=0;
the same init/config is re-authored via the new shared build_config/
build_init_params with only avg/max bitrate + VBV (PUNKTFUNK_VBV_FRAMES)
moved. On-hardware test: 20→60→10 Mbps in place, zero IDRs (RTX 5070 Ti).
- Native AMF: TargetBitrate/PeakBitrate/VBVBufferSize are dynamic
properties — SetProperty on the live component, no Terminate/re-Init.
- Vulkan Video (HEVC + AV1): stage the rate and emit an
ENCODE_RATE_CONTROL control command on the next recorded frame (begin
keeps declaring the session's current state, as the spec requires).
The session glue tries the in-place retarget first and skips the rebuild/
inflight-clear/IDR-cooldown bookkeeping when it succeeds — the reference
chain and the wire-index prediction survive, so RFI keeps working across
rate steps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mesa now exposes Vulkan Video decode queues by default (and the session
binary opts RADV in for the Deck's sake), which silently moved every desktop
AMD/Intel box onto FFmpeg-Vulkan-on-Mesa under `auto` — user-reported
(CachyOS/KDE) to judder or error-streak into the software demotion while an
explicit VAAPI pick streams perfectly. Auto's hardware order is now
device-aware (`VulkanDecodeDevice::prefer_vulkan_over_vaapi`, fed
vendor id + device name by the presenter): Vulkan-first stays only where it
is the established right answer — NVIDIA (no usable VAAPI) and the Deck's
VanGogh (VAAPI dmabuf import chroma-fringes) — and everything else gets the
battle-tested zero-copy VAAPI first, with Vulkan as its fallback.
A mid-session Vulkan failure streak now also demotes to VAAPI before
software, so a broken Mesa Vulkan path can never strand a box with a
perfectly good VAAPI driver on CPU decode.
The GTK shell's decoder setting gains the missing "Vulkan Video" option
(values now mirror the console UI's auto/vulkan/vaapi/software) and drops
its pre-Vulkan "Automatic (VAAPI → software)" label.
Verified on the RTX 5070 Ti box (loopback session, auto → "Vulkan Video
hardware decode active", 60 fps); policy locked by unit test; clippy -D
warnings + pf-client-core/pf-presenter tests green on Linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ABR ceiling was the negotiated start rate, so an 'Automatic' session
was permanently boxed at the 20 Mbps default no matter the link — the
most user-visible cap left after the transport work lifted the client
receive ceiling to ~4.8 Gbps wire.
- Startup link-capacity probe: ~2 s into an Automatic session the pump
fires one speed-test burst (2 Gbps target, 800 ms) over the existing
ProbeRequest machinery; delivered wire throughput x0.7 (FEC + variance
headroom) becomes the controller's climb ceiling via set_ceiling().
Old hosts decline (all-zero reply) or never answer (a 6 s timeout
clears the stuck probe state so LossReports resume) — the ceiling then
stays negotiated, exactly the old behavior. PUNKTFUNK_ABR_PROBE=0
opts out.
- Slow start: until the first congestion signal, every cooled clean
window DOUBLES the rate toward the ceiling (20 Mbps -> 640 Mbps in
~10 s) instead of +6% per ~10 s (which would have taken ~10 minutes).
Any congestion signal ends it for good; classic AIMD takes over.
- Faster, severity-aware AIMD: a SEVERE window (unrecoverable frame,
jump-to-live flush, or >=6% loss) backs off x0.7 immediately instead
of waiting two windows; ordinary congestion (2-6% loss, OWD rise)
keeps the two-window fuse. Additive climbs need 6 clean windows
(~4.5 s, was ~10 s); the change cooldown drops 3 s -> 1.5 s.
- PUNKTFUNK_VBV_FRAMES now also scales the direct-NVENC VBV (Windows +
Linux, previously hardwired to 1 frame) — parity with AMF/VAAPI/QSV.
Each accepted step still costs an encoder rebuild + IDR on the host;
in-place rate reconfigure (NvEncReconfigureEncoder / AMF dynamic
properties / Vulkan per-frame RC) is the planned follow-up that makes
stepping free. Controller tests rewritten to the new policy (severity
classes, slow-start climb, ceiling semantics; 144 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- REPLAY_WINDOW 32768 -> 131072: the anti-replay bitmap covered the
120 ms loss window only to ~2 Gbps; the client now delivers ~4.8 Gbps
wire, where a late-but-valid Wi-Fi-retried datagram would have been
dropped as 'older than the window' — false loss. 16 KiB/session
covers ~12 Gbps.
- RECV_BATCH 32 -> 128: syscall rate stays ~3.4k/s at 430k pkt/s and
each pump iteration drains the kernel buffer deeper (ring 64->256 KB,
client sessions only). flush_backlog's iteration cap rescaled to keep
its ~190 MB guard equivalent.
- PUNKTFUNK_GSO gate is now value-aware: '=0' used to ENABLE GSO on
Linux (presence check) while disabling Windows USO. GSO stays OPT-IN,
deliberately: A/B'd twice today — it cuts send-thread CPU ~30% but
its 16-packet line-rate trains cost delivered throughput on a
constrained fabric (2.5GbE-hop pair: peak 2453 -> 1908 Mbps and 0.4%
loss at a rate sendmmsg carries clean). Flipping the default belongs
with pace-aware chunk spacing (plan Phase 1.2/1.3). docs-site row
corrected to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RustCrypto aes 0.8.x and polyval 0.6.x gate their ARMv8 AES / PMULL
paths behind --cfg aes_armv8 / --cfg polyval_armv8 on aarch64 (x86_64
runtime-detects AES-NI with no flag, which is why hosts never showed
it). Without the cfgs every Apple and Android client decrypted the
media plane in SOFTWARE: 240 MiB/s/core measured on an M3 Ultra —
7 µs per 1.4 KB datagram, single-handedly capping receive throughput
at ~1.57 Gbps wire on both host pairs.
Workspace .cargo/config.toml sets both cfgs for
cfg(target_arch = "aarch64"); detection stays runtime (cpufeatures)
with a safe soft fallback. open_in_place: 240 MiB/s -> 2.42 GiB/s
(10.3x). Live sweep .173 -> M3 Ultra over 10GbE: ceiling 1572 ->
4830 Mbps wire, zero loss through a 3.5 Gbps target; the .21 pair now
saturates its physical 2.4 Gbps fabric exactly.
No in-tree build path sets RUSTFLAGS (xcframework + gradle checked),
so the config reaches all client builds; a lane that sets RUSTFLAGS
overrides config rustflags entirely and must carry the cfgs itself
(noted in the file). Shipping Apple/Android binaries stay on software
crypto until rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the client Reassembler around one whole-frame buffer per frame:
frame_bytes rides in every header and packetize geometry is
deterministic (every non-final block is exactly max_data_per_block data
shards), so a data shard's final AU offset is computable on arrival —
copy it there once, straight from the decrypt ring. New
ErasureCoder::reconstruct_into decodes ONLY the missing shards directly
into the frame buffer's holes (gf16 native; gf8 legacy shim); received
recovery shards ride pooled shard-sized buffers. The completed buffer
IS Frame::data.
Deletes the per-shard to_vec + per-block concat + final AU concat
(~178k allocs and a double copy of every byte per second at 2 Gbps —
the pump wall the 2026-07-14 sweeps measured at 98.9% of an M3 Ultra
core). Reassembly now costs ~0.4 µs/packet in-stream.
The eager buffer changes the hostile-header exposure, so two new
firewalls: derived-geometry validation (a header lying about its
data_shards/block_count vs its own frame_bytes is dropped before it can
scribble across another shard's range) and an in-flight allocation
budget (IN_FLIGHT_BUF_FACTOR × max_frame_bytes) so a window of tiny
first-shards can't commit gigabytes.
Behavior parity pinned by the existing suite (all green unchanged) plus
new end-to-end roundtrips through the real Packetizer (multi-block +
partial tail, loss within budget, reversed delivery, duplicates, empty
frame, unrecoverable block ages out, budget enforcement). loss-harness
recovery curve identical; pipeline bench: gf8/1MB +42%, gf16 neutral
(host-encode dominated). Known pre-existing quirk kept as-is: reversed
delivery reconstructs early (data+recovery ≥ k) and counts late-not-lost
shards into fec_recovered_shards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three bugs found running the owed throughput sweeps (all three conspired
to make yesterday's 'transport does 1G+' numbers fabrications):
- the probe never advertised VIDEO_CAP_PROBE_SEQ, so every host DECLINED
its speed tests; the zeroed decline reply divided a settle-window
sliver by 1 ms and printed plausible-looking garbage. Advertise the
cap (the shared-core reassembler windows probe-space frames) and
detect the all-zero decline explicitly.
- an idle virtual desktop publishes no frames on damage-driven capture
(Windows IDD-push), so the pipeline build timed out before the burst
could run. The probe now injects a ±2 px cursor wiggle over the wire
during --speed-test warmup — injected host-side into the right
session, works headless everywhere.
- throughput-sweep.py: tracing emits ANSI color into pipes, which broke
the key=value parser (crash on the first point); strip it, guard
half-parsed lines, and surface host declines as a flag.
Also logs the whole-run receive stage split (PUNKTFUNK_PERF) at stream
end — the probe is the measurement tool for the client-pump wall.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A host (re)started after a desktop login inherits the user manager's
compositor env; a stale WAYLAND_DISPLAY makes headless gamescope 3.16
exit at startup ('Failed to connect to wayland socket') before its
PipeWire node appears. Unset both on the systemd-run transient unit
(UnsetEnvironment=) and the direct spawn (env_remove) — gamescope
exports its own DISPLAY/GAMESCOPE_WAYLAND_DISPLAY to the nested app.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session::poll_frame accumulates per-stage ns (recv_batch syscall, AES-GCM
open, Reassembler::push incl. FEC) into a PumpPerf drained via
take_pump_perf(); the client pump logs the split plus completed-AU
inter-arrival jitter (p50/p95/max + late count) every report window.
Gated on PUNKTFUNK_PERF — one branch per stage when off.
Smoothness previously had no metric at all (jump-to-live counters fire
seconds late), and the receive core had no attribution. First live use
pinned the 1.57 Gbps client wall on software AES-GCM (7 µs/pkt) vs
0.4 µs reassembly — see punktfunk-planning/design/throughput-beyond-1gbps.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone sweep to probe the ~500 Mbps throughput wall (transport vs encoder
CBR undershoot); built and validated, no runtime coupling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
B1b: a Steam running in a plain GNOME/KDE desktop session holds Steam's single
instance, so a dedicated gamescope launch's own Steam exits at birth — the
game-library launch goes to a black screen. Release the desktop instance
(free_desktop_steam) on Steam launches before creating the managed session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iOS + Android: a new opt-in setting mirrors controller 1's rumble onto the
device's own actuator (Apple RumbleRenderer Actuator.device / CoreHaptics,
Android deviceBodyVibrator), so a motor-less clip-on pad still gives haptic
feedback through the phone/tablet it's clamped to. Default off; wired through
the gamepad settings on both platforms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iOS + Android: a three-finger vertical swipe up/down summons/dismisses the
device soft keyboard while streaming (trackpad + pointer modes). Mobile scroll
is now exactly two fingers so it never collides with the 3+-finger gesture
(3+ only fell into the old `>= 2` scroll path by accident).
Android: a TYPE_NULL KeyCaptureView plus IME meta-shift wrapping feeds key
events through. iOS: UIKeyInput plus a SoftKeyMap char->VK table with a
GCKeyboard dup gate so a hardware keyboard and the soft keyboard don't
double-emit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ship a Steam Input controller layout (controller_config/punktfunk.vdf) whose
always-on `ts_n` command enables native touchscreen delivery on the Deck, and
have the backend auto-install it (apply_controller_config: copy to
controller_base/templates + upsert the per-account configset entry, chown to the
user, back up first). This is what makes the Deck touchscreen reach the client
as native touch under gamescope without disabling Steam Input (impossible on the
Deck) — no manual controller setup.
Two shortcuts sharing the "Punktfunk" name (so one config key covers both): a
hidden stateful stream entry and a visible stateless entry that launches straight
into the gamepad UI. Both get full artwork (grid/gridwide/hero/logo/icon,
replaced with exported PNGs). Drop the art-generation script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the gamepad/console shell (pf-console-ui) to parity with the other clients:
- Settings gain a Touchscreen → Touch mode row (Trackpad / Direct pointer /
Touch passthrough), the one couch-relevant Settings field the screen lacked.
- The pair screen adds the no-PIN delegated-approval path: a "Request access"
action (only when the host advertises a fingerprint to pin) opens a connect the
host PARKS until the operator approves this device, then persists it as paired.
A role-based row model keeps the cursor off stale indices; manual hosts stay
PIN-only, matching the desktop shells.
- Threads request_access through OverlayAction::Launch and ConnectIntent; the
shell shows a "Waiting for approval…" takeover, and the session binary parks on
a 185 s budget (PendingApproval → persist-as-paired via on_connected).
Auto-wake (WoL) was already implemented end-to-end and is left as-is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Steam validates the Deck unit serial's format before accepting it. Our
"PFDK..." serial was REJECTED ("Invalid or missing unit serial number"), so
Steam substituted a hash identity and mangled the displayed name to
"Steam Deck Controllerggg" on every host tested. An 'F'-leading serial passes,
so switch to "FVPF..." — keeps the PunktFunk marker one slot in, still distinct
from a real Deck's "FVZZ..." for the Linux self-detection in
physical_steam_controller_present(). The name now shows a clean "Steam Deck
Controller" with a serial-derived handle (verified on .173).
Also fix the UMDF driver's 0xAE GET_STRING_ATTRIBUTE handler to echo the
requested attribute id faithfully instead of collapsing board-serial (0x00)
requests to unit-serial (0x01). Steam still logs a benign "Deck Controller PCB
Serial# invalid" for the board serial — it validates that against a
Valve-internal format for ANY value, including an empty one (verified) — but
that line does not mangle the name, change the handle, or block promotion.
Applied to both transports: host inject/proto/steam_proto.rs::deck_serial
(Linux gadget/usbip) and the pf-dualsense UMDF driver (Windows), which mirror
each other's serial format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The N4 GO verdict, productized. GamepadPref::SteamDeck on a Windows host
now builds a real virtual Deck instead of folding to DualSense: games
get native Deck glyphs + both trackpads + gyro + all four back grips
through Steam Input's own remapping.
- steam_deck_windows.rs: DeckWinPad/DeckWinProto/SteamDeckWindowsManager
over the sealed shm channel, sharing the whole Linux Deck codec
(steam_proto now compiles on Windows too — it was already pure). The
SwDevice identity carries usb_mi: Some(2): the &MI_02 hardware-id
token hidclass mirrors into the HID child and Steam parses as the
wired controller interface — the promotion gate.
- Driver: DEVTYPE_STEAMDECK (3) graduates from the spike — SET_FEATURE
0xEB rumble / 0x8F haptic pulses are republished to the host through
the output slot (report-id-0 prefixed, so parse_steam_output sees the
Linux wire shape), and the 0xAE/GET_STRING serial + 0x83 unit id are
per-pad (read from the section's pad_index; PFDK<unit-id> matches
steam_proto::deck_serial).
- Router: SteamDeck arms in the Windows Pads paths; pick_gamepad flips
SteamDeck-if-windows -> SteamDeck (the DualSense fold retires);
dualsense-windows-test grows --deck.
ON-GLASS VALIDATED on .173 (rebuilt signed driver 9.9.0714.12xx
installed, Steam live): the manager-created pad (index 1) enumerates
with per-pad serial PFDK50460001, Steam logs Interface: 2 ->
'!! Steam controller device opened' -> 'Steam Controller reserving
XInput slot 0' -> PollState 2 (actively polling our cycling input
frames) -> mapping activated; clean teardown on exit. Rumble round-trip
through a real game remains an on-glass debt (nothing sent 0xEB during
the idle hold).
Known gap vs Linux: no physical-Steam-controller conflict degrade on
Windows yet (degrade_steam_on_conflict is Linux-only — /sys scan); a
Windows equivalent needs SetupDi enumeration and is deferred.
Verified: .21 clippy -D warnings + 304/0 tests + fmt --all; .133 clippy
-D warnings + the WDK driver-workspace check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three things that belong together:
1. rustfmt the gamepad-new-types host files ci.yml's `cargo fmt --all
--check` gate flags (the .21/.133 verify recipes ran clippy+tests
but never fmt — the same class of miss as 69f30f30).
2. Enforce it at the source: scripts/git-hooks/{pre-commit,pre-push}
run the exact CI fmt gates (main workspace + the shipped-driver
crates of the UMDF workspace); CONTRIBUTING documents the one-time
`git config core.hooksPath scripts/git-hooks`. pre-push is the
enforcement point (plumbing commits bypass pre-commit).
3. N4 follow-up — the spike verdict FLIPS TO GO: SwDeviceProfile grows
`usb_mi`, synthesizing `&MI_02` into the Deck spike's USB hardware
ids. hidclass mirrors the parent's USB tokens into the HID child's
hardware ids, and hidapi/SDL/Steam parse `MI_` as bInterfaceNumber
(defaulting to 0 when absent — the exact gate the first run hit:
Steam wants the Deck controller on interface 2). Re-run live on
.173: Steam logs `Interface: 2`, then `!! Steam controller device
opened`, `Steam Controller reserving XInput slot 0`, and activates
a mapping — full Steam Input promotion of the software-devnode
Deck, no driver change needed. The PS identities pass
`usb_mi: None` (real single-interface devices carry no MI_ token).
A proper Windows-Deck backend phase is now justified; planned
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
057E:2069 (Pro Controller 2) and 057E:2068 (Joy-Con 2 pair) are the
same full pad surface as the OG Pro and ride the same virtual
hid-nintendo pad. Mirrors SDL, which folds both to its public
NINTENDO_SWITCH_PRO type (the SDL clients bundle 3.4.10, whose switch2
hidapi driver already covers them end to end incl. gyro + GL/GR
paddles-as-paddle-buttons). :kit Kotlin compile green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RECORD_AUDIO / Wi-Fi-state permissions implied hard microphone + wifi
requirements, filtering mic-less TVs (reported: Philips OLED707) and
ethernet-only boxes as "not compatible" on Play; both are optional at
runtime and now declared required=false (aapt2-verified).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Used for the .173 on-glass verify: the Edge devnode enumerates
(SWD\PUNKTFUNK\PF_EDGE_1, driver pf_dualsenseedge attaches, proto 2),
and Steam's live controller.txt confirms the identity end to end —
'type: 054c 0df2', 'Product: DualSense Edge Wireless Controller',
'Controller using HIDAPI driver, vid=0x054c, pid=0x0df2' — with probe
lightbar/player-LED feedback flowing back on the 0xCD plane.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gamepad-new-types §6 go/no-go rig, ready to run the moment .173 is
back (the box is currently down, so the observation itself is still
owed): does Steam Input on Windows promote a software-devnode HID Deck
(28DE:1205), or does it need a real USB bus identity (the documented
GameInput instance-path gap — the Linux 'Interface: -1' lesson)?
- Driver: scratch device_type=3 serves the Deck identity — the captured
38-byte controller-interface descriptor, 28DE:1205 attributes, Valve
strings, the Deck neutral frame, and the Steam 0x83/0xAE feature
contract (SET_FEATURE latches the command, GET_FEATURE answers it —
attribute blob + unit serial mirroring steam_proto::feature_reply).
Never stamped by a session. INF gains pf_steamdeck.
- Host: deck_spike_hold() + the `deck-windows-spike` subcommand — stamps
devtype 3, spawns the devnode under VID_28DE&PID_1205, streams the
neutral frame, prints what to observe (Steam logs/controller.txt,
controller settings) and logs any output reports Steam writes.
Run recipe (on .173, once the updated signed driver is staged): install
driver, start Steam, `punktfunk-host.exe deck-windows-spike`, watch
controller.txt. GO -> plan a proper N4 phase (the Deck codec is already
shared); NO-GO -> document next to the Linux Interface:-1 note and keep
the SteamDeck->DualSense Windows fold.
Verified: .133 clippy -D warnings + the driver workspace cargo check
(WDK) both green; .21 clippy + 304/0 tests unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan 0.4 for the N1/N2 backends (SDL landed with them):
- Apple: GamepadType grows dualSenseEdge=7 / switchPro=8 (wire-byte
parity + name parsing). padKind splits the Edge out of the shared
GCDualSenseGamepad subclass by product category, and resolves Switch
Pro / a paired Joy-Con set by category (GameController has no Nintendo
subclass; single Joy-Cons stay on the Xbox 360 fallback — half a pad).
The DualSense-only gates (adaptive-trigger feedback, player LEDs, the
touchpad+motion rich capture) now include the Edge — same surfaces.
Paddle CAPTURE stays gated on G22 (needs a real pad to pin the
paddleButton1..4 correspondence); the declared identity is right
meanwhile. swift build + 124 tests green.
- Android: PREF_DUALSENSEEDGE/PREF_SWITCHPRO wire bytes; the Sony PID
table splits 0x0DF2 (Edge) out of DualSense; Nintendo 057E:2009
declares Switch Pro; ControllersScreen labels the new kinds.
:kit/:app Kotlin compile green (-PskipRustBuild).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reserved GamepadPref::SteamController = 5 slot goes live: the same
hid-steam driver under the wired-SC identity (28DE:1102,
ID_CONTROLLER_STATE), UHID-only in v1 (no captured SC USB interface
layout, so no Steam-Input promotion — the pre-usbip Deck state;
acceptable for discontinued hardware).
Layout pinned against the kernel's ID_CONTROLLER_STATE table: 24-bit
buttons at 8..11 (low bits shared with the Deck; grips at 9.7/10.0 =
the Deck's L5/R5 positions; right-pad click 10.2; joystick click 10.6),
u8 triggers at 11/12, the joystick/left-pad MULTIPLEX at 16..20 (a
left-pad contact shadows the stick, like real hardware's lpad_touched
flag), right pad at 20..24. Mapping: wire left stick -> SC stick; wire
right stick -> right-pad coords + touched bit (the SC's camera surface —
the second-stick loss is inherent); PADDLE1/2 -> the two grips (natively,
masked out of the fold input); PADDLE3/4 + MISC1 -> the remap policy.
The SC parser has NO gamepad_mode gate, so no mode-entry pulse.
SteamDeckPad grew a SteamModel (open_model); ScProto/SteamCtrlManager;
pick_gamepad flips SteamController -> itself on Linux (replacing the
Xbox360 fold); SDL picker splits Valve PIDs (Deck 1205 stays SteamDeck,
SC 1102/1142 now declare SteamController).
Verified: .21 clippy -D warnings + 304/0 tests + on-box UHID smoke
(hid-steam binds 1102, BTN_A + right-pad ABS_RX land on evdev, no mode
pulse); .133 clippy -D warnings green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RECORD_AUDIO implies android.hardware.microphone required=true and the
Wi-Fi state permissions imply android.hardware.wifi required=true unless
declared otherwise, so Google Play filtered the app as "not compatible"
on TVs that declare no microphone (reported on a Philips 65OLED707/12,
Android TV 11, closed-testing track) and would do the same on
ethernet-only boxes. Both capabilities are optional at runtime: the mic
uplink is runtime-requested and the Wi-Fi locks are best-effort hedges.
Verified via aapt2 dump badging: microphone + wifi now report
uses-feature-not-required and no implied hard requirements remain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0 of gamepad-new-types: the two new kinds exist on the wire (enum,
to_u8/from_u8/from_name/as_str, C-ABI constants + header), and pick_gamepad
folds them to the closest EXISTING backend until their own backends land —
DualSenseEdge -> DualSense (keeps the rich planes; only the paddles go
through the fold policy), SwitchPro -> Xbox360. Wire round-trip pinned
0..=8 + unknown->Auto; fold table extended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deferred Phase 3.3 of the gamepad review (gamepad-review-cleanup.md
§3a): the seven virtual-pad managers' copy-pasted lifecycle (slot table,
active_mask unplug sweep, gate-checked create, rumble/hidout dedup,
heartbeat) extracted into shared PadSlots<P> + PadProto/UhidManager<B>;
each backend now supplies only its protocol half via a type alias, with
zero Pads-router edits. Includes the 3.3.0 pre-step fixing the drifted
Linux DS4 backend (rich-plane pad clicks + the Steam left pad were dead
on the DS4 kind).
10 commits, each verified as it landed: Linux .21 clippy -D warnings +
full host suite 290 pass / 0 fail + fmt; Windows CI VM .133 clippy
--all-targets -D warnings EXITCODE 0. On-glass kind-cycling smoke
(one real pad per platform) still owed post-merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the vestigial Ds4Feedback.hidout field (parse_ds4_output never
filled it and neither DS4 manager read it — the lightbar rides the led
field, now converted to a HidOutput::Led by the protos) and its
now-unused HidOutput import; refresh the pad_gate module doc (managers
now drive it via pad_slots).
Verified: .21 clippy --all-targets -D warnings + full suite 290 pass /
0 fail + cargo fmt --check clean; .133 clippy --all-targets -D warnings
EXITCODE 0.
Part of G12/3.3 (§3a.4 commit 10) — extraction complete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two stateless backends keep their structs and special pumps (uinput
FF-effect mixing via pump_ff/last_mix; the XUSB stale-residual
RUMBLE_IDLE_TIMEOUT force-off) but delegate slot lifecycle — table,
unplug sweep, gate-checked create — to the shared PadSlots. XUSB resets
last_rumble/last_active on the swept indices and on fresh create exactly
as before (the G10/G16-adjacent semantics untouched).
Two accepted deltas, both flagged in the plan (§3a): the uinput
arrival/unplug log lines gain the pad-identity label every other backend
already has ("controller arrival (X-Box 360 pad)"), and XUSB's
f.index.max(0) clamp is replaced by the bounds check every other manager
uses — a negative wire index is now dropped instead of being treated as
pad 0.
Verified: .21 clippy --all-targets -D warnings clean + full suite 290
pass / 0 fail (uinput); .133 clippy --all-targets -D warnings EXITCODE 0
(XUSB).
Part of G12/3.3 (§3a.4 commit 9) — all seven managers now share the
PadSlots lifecycle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The most hook-laden conversion: SteamControllerManager becomes a pub
type alias of UhidManager<SteamProto>. The Steam-specific pieces map
cleanly onto the trait — open() delegates to open_transport (usbip →
gadget → UHID fallback, which keeps its own per-transport logging, so no
extra success line, matching the old ensure), merge_frame preserves the
trackpad coords/touch-bits/clicks + motion across button-only frames
(the G2 fix, verbatim), and the gamepad-mode-entry pulse rides the
force_heartbeat hook. DeckTransport goes pub (type Pad in a public-trait
impl). Also un-fuses a doc-comment glitch where the manager's doc had
been merged onto the DeckTransport enum.
Verified on .21: clippy --all-targets -D warnings clean; full suite 290
pass / 0 fail.
Part of G12/3.3 (§3a.4 commit 8) — all five stateful managers now share
one skeleton.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DualShock4Manager becomes a pub type alias of UhidManager<Ds4LinuxProto>
(the same shape as the other three DS-family conversions); the bespoke
last_led lightbar dedup folds into the shared HidoutDedup exactly as the
Windows DS4 conversion did. With 3.3.0 already applied, the proto half
is byte-identical to Ds4WinProto except the transport open — the codec,
the mappers, and now the manager all shared.
Verified on .21: clippy --all-targets -D warnings clean; full suite 290
pass / 0 fail.
Part of G12/3.3 (§3a.4 commit 7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DualShock4WindowsManager becomes a pub type alias of
UhidManager<Ds4WinProto>. The bespoke last_led lightbar dedup folds into
the shared HidoutDedup: the proto's service() converts Ds4Feedback.led
into a HidOutput::Led, and HidoutDedup compares it against the
last-forwarded value with the same reset-on-create/unplug semantics the
Option<(u8,u8,u8)> vec had. Everything else mirrors the DualSense
conversion (same DsState mappers as linux/dualshock4.rs). Ds4WinPad goes
pub (type Pad in a public-trait impl, E0446 otherwise).
Verified on the Windows CI VM .133: cargo clippy -p punktfunk-host
--all-targets -- -D warnings EXITCODE 0 at this tip.
Part of G12/3.3 (§3a.4 commit 6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>