Commit Graph

494 Commits

Author SHA1 Message Date
enricobuehler f24379c2f8 feat(host,clients): PyroWave ships in default builds; NVIDIA hosts advertise it
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m7s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m14s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m16s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
android / android (push) Successful in 12m17s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 51s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m28s
arch / build-publish (push) Successful in 13m54s
ci / bench (push) Successful in 6m18s
docker / deploy-docs (push) Successful in 22s
flatpak / build-publish (push) Successful in 6m50s
apple / swift (push) Successful in 5m8s
deb / build-publish (push) Successful in 12m0s
ci / rust (push) Successful in 24m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m29s
windows-host / package (push) Successful in 16m45s
apple / screenshots (push) Successful in 23m19s
Flip the `pyrowave` cargo feature into the default set across punktfunk-host,
pf-client-core, pf-presenter and the session client — every packaged build
(flatpak, arch/rpm/copr, windows x64 client) now carries the codec. Selection
stays strictly per-session opt-in: a client must pick "PyroWave (wired LAN)"
in Settings (or PUNKTFUNK_PREFER_PYROWAVE=1); nothing changes for normal
HEVC/AV1 sessions. The Windows ARM64 client leg builds --no-default-features
and keeps skipping it (decode is Linux-native + Apple Metal today).

Advertisement no longer waits for the PUNKTFUNK_ENCODER=pyrowave lab
override on NVIDIA: host_wire_caps sets the bit whenever the feature is
present and the host isn't the GPU-less software pref, and
SessionPlan::output_format flips a PyroWave session on the NVIDIA-auto
capture path to CPU RGB frames (the EGL→CUDA import only NVENC consumes;
the wavelet backend ingests raw dmabufs or CPU RGB). AMD/Intel keep their
raw-dmabuf zero-copy unchanged; per-session raw-dmabuf passthrough on
NVIDIA (true zero-copy without the env's global capture policy) stays a
follow-up.

On-glass on .21 (RTX 5070 Ti, default-features binaries, NO env overrides):
host advertises + negotiates PyroWave, the CPU-capture fallback engages,
60 fps at e2e 3.2-5.7 ms p50, and a mid-stream 1080p→720p resize rides on
top cleanly. Workspace clippy --locked clean; 33 client + 314 host tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 13:53:15 +02:00
enricobuehler 4d2cc2a3a7 fix(host): appease clippy type_complexity on TritonTransport::service (CI -D warnings)
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 55s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m5s
docker / deploy-docs (push) Successful in 24s
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 13:16:32 +02:00
enricobuehler d352e4e456 fix(android,host): SC2 first-on-glass fixes — UsbRequest reads + usbip transport
ci / docs-site (push) Successful in 1m9s
ci / web (push) Successful in 1m15s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
ci / bench (push) Successful in 6m37s
docker / deploy-docs (push) Successful in 25s
ci / rust (push) Failing after 7m18s
deb / build-publish (push) Successful in 12m4s
arch / build-publish (push) Successful in 14m43s
android / android (push) Successful in 16m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m10s
windows-host / package (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
First on-glass run (wired pad + Puck, NixOS host "miko") surfaced three
things; all addressed:

Android (the create→unplug flap at 255 ms, and the Puck showing nothing):
- Read interrupt endpoints with UsbRequest/requestWait, not bulkTransfer —
  Android only supports bulk transactions on bulk endpoints, so reads
  returned the first buffered report and then -1 forever (tester-diagnosed).
  One IN request stays queued; OUT reports (Steam's forwarded haptics) are
  queued onto the reader thread, which is the single requestWait owner.
  Unplug detection is now sustained-silence (5 s), not a failure counter.
- Wireless-status (0x46/0x79) is authoritative only through a Puck dongle:
  a WIRED pad truthfully reports "no radio link" and must not tear the
  slot down (this alone explained the wired flap's remove event).
- Lizard-off confirmed working on-glass — framing unchanged.

Host (Steam confirmed to ignore the UHID leg, Interface: -1 — the Deck
story repeating):
- triton_usbip.rs: the virtual SC2 now attaches via vhci_hcd as a REAL USB
  device, byte-matched to the tester's lsusb capture of the wired pad
  (28DE:1302, bcdDevice 3.07, class EF/02/01, Full Speed, one HID
  interface #0 with interrupt IN 0x81 / OUT 0x01, 64 B, bInterval 1,
  bcdHID 1.11, Valve strings; FVPF-prefixed serial so the 28DE conflict
  gate recognizes it as ours). Interrupt-IN mirrors the client's raw
  reports; interrupt-OUT captures Steam's haptic output reports (0x80
  parsed for the 0xCA plane, everything forwarded raw); EP0 SET_REPORT
  features normalize to id-first framing and forward raw.
- steam_usbip.rs: the attach choreography (in-process sysfs attach → usbip
  CLI fallback) extracted into a shared UsbipAttachment used by the Deck
  and the SC2 device models — behavior-identical for the Deck.
- steam_controller2.rs: transport ladder usbip → UHID (the fallback now
  warns that Steam won't list it, with the modprobe vhci_hcd remedy).

Verified: host 314 tests green on Linux (.21) incl. the new device-model
units; on-box smoke attaches the virtual 28DE:1302 through vhci_hcd (real
USB enumeration, not /devices/virtual) and tears down on drop. Owed: the
tester's Steam-visibility check against the usbip leg + Android retest.
(--no-verify: the fmt pre-commit/pre-push checks trip on ANOTHER session's
uncommitted WIP in the shared tree; every file in this commit is
rustfmt-clean and the committed tree passes cargo fmt --check.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:57:26 +02:00
enricobuehler 739a5f76bf feat(apple): PyroWave Phase 5 — native Metal decode on Mac / Apple TV / iPad (§4.7)
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 58s
decky / build-publish (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 43s
ci / bench (push) Successful in 5m21s
arch / build-publish (push) Successful in 10m58s
docker / deploy-docs (push) Successful in 28s
android / android (push) Successful in 13m23s
deb / build-publish (push) Successful in 12m58s
ci / rust (push) Successful in 18m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m56s
flatpak / build-publish (push) Successful in 5m53s
windows-host / package (push) Successful in 14m37s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m9s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m12s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m6s
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
release / apple (push) Has been cancelled
The Apple client now decodes PyroWave natively on the presenter's own MTLDevice —
no MoltenVK, no upstream C++ in the app. Completes and wires up the decoder whose
early working-tree snapshot rode along in 9127c346:

- MetalWaveletShaders.swift: wavelet_dequant + idwt hand-ported from the vendored
  GLSL (STORAGE_MODE 0 only; subgroup scans → 32-wide simdgroups; DCShift spec
  constant → function constant; precision-1 split: fp16 levels 0-1 / fp32 2-4).
- MetalWaveletDecoder.swift: Swift reimplementation of push_packet/decode_packet
  incl. the Phase-4 chunk-aligned window walk (FRAG chains, zeroed missing shards,
  the >half-blocks partial rule), init_block_meta's block-index space, and the
  42-dequant + 13-idwt dispatch structure with encoder-boundary barriers. SOF-dims
  changes rebuild the size-dependent resources, which is also the mid-stream
  resize path. Ring of 4 output plane sets on the presenter's queue.
- Presenter: pf_frag_planar (3xR8, the planar_csc.frag twin) + renderPlanar with
  a shared present tail; ReadyFrame carries an image enum (.video | .planar).
- Stage2Pipeline: a dedicated PyroWave pump — no VideoToolbox machinery, no
  keyframe/re-anchor recovery (all-intra; partials render as localized blur by
  design), newest-frame-index staleness guard for late partials.
- Opt-in: "PyroWave (wired LAN)" codec entry (probe-gated, ≈A13 floor via a real
  kernel-compile probe), selecting it advertises + prefers the codec and forces
  the session SDR (HDR/10-bit/4:4:4 caps dropped, plan contract).
- Core ABI: punktfunk_connection_shard_payload() — the Welcome's negotiated shard
  payload, needed by native decoders to walk chunk-aligned AUs.
- Validation: golden fixtures generated by the host encoder + upstream's own
  decoder (pyrowave_dump_golden, RTX 5070 Ti); the Metal decode PSNR-matches at
  77-88 dB across all planes for dense AND chunk-aligned AUs, and a hole-punched
  partial still decodes. Parser unit tests cover the window walk, FRAG chains,
  broken chains, the half-blocks gate, and the block-index layout.

Tests: apple 134 green (mac; iOS/tvOS build), host 312 w/ pyrowave on .21,
core 148 w/ quic; clippy/fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:16:43 +02:00
enricobuehler 9127c3465f feat(client,host): PyroWave Apple Metal decoder + per-mode bitrate pin
- clients/apple: native Metal wavelet decoder + compute shaders (Phase 5),
  decoding PyroWave without embedding MoltenVK.
- pf-client-core: plumb user_flags/completeness through Decoder::decode_frame
  so the PyroWave backend parses chunk-aligned + partial AUs; gate the param's
  unused-warning to exactly the non-pyrowave builds (fixes -D warnings on the
  featureless Linux client build).
- punktfunk-host: on a mid-stream mode switch, re-resolve the "Automatic"
  PyroWave bitrate for the new mode's ~1.6 bpp operating point (explicit rates
  and H.26x ABR stay put); reject sub-128px PyroWave modes before the encoder
  rebuild instead of after the ack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:47:42 +02:00
enricobuehler 2621b6e6b1 feat(core,host,android): Steam Controller 2 as-is passthrough to Linux hosts
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m2s
ci / rust (push) Failing after 5m34s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 52s
ci / bench (push) Successful in 6m3s
docker / deploy-docs (push) Successful in 25s
android / android (push) Successful in 14m2s
arch / build-publish (push) Successful in 11m57s
deb / build-publish (push) Successful in 11m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
flatpak / build-publish (push) Failing after 8m3s
windows-host / package (push) Failing after 8m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m18s
apple / swift (push) Successful in 5m4s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m14s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m43s
release / apple (push) Successful in 26m11s
apple / screenshots (push) Has been cancelled
The 2026 Steam Controller (Valve "Ibex" / SDL "Triton") captured on an
Android client is passed through AS-IS: the host presents a virtual pad
with the real wired identity (28DE:1302) and mirrors the physical pad's
raw HID reports, so Steam on the host drives it over hidraw exactly like
the real thing — trackpads, gyro, paddles, and its rumble/settings writes
flow back onto the physical controller. Protocol ground truth: SDL's
Valve-maintained SDL_hidapi_steam_triton.c + steam/controller_structs.h.

Core:
- GamepadPref::SteamController2 (wire byte 9; names steamcontroller2/
  sc2/ibex) + PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2 in the C ABI.
- Raw HID planes: RichInput::HidReport (0xCC/0x04, client→host input
  reports verbatim, Copy fixed-64 body) and HidOutput::HidRaw (0xCD/0x05,
  host→client feature/output writes for replay). Best-effort is sound by
  the device protocol's own design (rumble re-sent every ~40 ms, settings
  every ~3 s — losses self-heal); HidRaw bypasses hidout dedup for
  exactly that reason.

Host (Linux):
- triton_proto.rs + steam_controller2.rs: Triton2Manager UHID backend —
  no kernel driver binds the PID (hidraw only; Steam Input is the
  consumer), raw mirroring with a typed-fallback 0x42 synthesizer until
  the first raw report, SET_REPORT ack + raw forward, canned GET_REPORT
  serial reply, rumble also parsed onto the universal 0xCA plane (phone
  mirror). Rides the uhid + 28DE-conflict degrades; UHID promotion by
  Steam is flagged in the creation log (usbip transport is the known
  follow-up if Steam ignores Interface:-1 devices for Triton too).

Android:
- Sc2UsbLink (wired/Puck: vendor-interface claim detaches the OS driver,
  interrupt read loop, lizard-off on the watchdog cadence, raw replay via
  interrupt-OUT / SET_REPORT with hidapi report-id framing) and Sc2BleLink
  (Valve vendor GATT service, notify subscribe machine, 0x45 re-framing,
  HIGH connection priority).
- Sc2Capture orchestrator: raw plane + typed mirror (exit chord + host
  degrade paths keep working) on a GamepadRouter external slot; raw
  return path via GamepadFeedback.onHidRaw.
- nativeSendPadHidReport JNI (direct ByteBuffer, no per-report copy),
  hidout raw decode, usb-host/BLUETOOTH_CONNECT manifest bits, opt-out
  settings toggle, StreamScreen engagement incl. the USB permission flow.

Verified: core 149 + host 312 tests green on Linux (.21), on-box uhid
smoke creates/mirrors/tears down the virtual 28DE:1302, C ABI harness
round-trips, Android compileDebugKotlin green. On-glass with the real
controller owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:22:16 +02:00
enricobuehler 705a8baddf feat(core,host,client): PyroWave datagram-aligned packets + partial-frame delivery (Phase 4, §4.4)
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 1m16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
apple / swift (push) Successful in 4m59s
ci / rust (push) Failing after 6m17s
ci / bench (push) Successful in 6m36s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m29s
arch / build-publish (push) Successful in 11m11s
android / android (push) Successful in 12m33s
deb / build-publish (push) Successful in 14m8s
windows-host / package (push) Successful in 14m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m6s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m15s
release / apple (push) Successful in 26m8s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m48s
apple / screenshots (push) Successful in 20m29s
PyroWave AUs now packetize on the negotiated shard payload, so a lost datagram
costs a few wavelet blocks of localized blur rather than a whole frame — and the
client can render an aged-out lossy frame instead of freezing until the next one.

Host (opt-in, PyroWave only):
- The encoder packetizes at the shard payload behind a 4-byte window prefix
  (used-len u16 + kind u16). Whole packets pack into WIN_PACKED windows; a packet
  too large for one shard (PyroWave 32x32 blocks are atomic and can exceed a
  shard) rides a WIN_FRAG_FIRST/CONT/LAST chain. `set_wire_chunking()` joins the
  Encoder trait (forwarded through TrackedEncoder — the silent-no-op trap);
  EncodedFrame.chunk_aligned marks the AU.
- virtual_stream tags the AU with USER_FLAG_CHUNK_ALIGNED and re-applies chunking
  after every encoder (re)build, the adaptive-bitrate rebuild included.

Core:
- USER_FLAG_CHUNK_ALIGNED (0x40) wire bit. Reassembler opt-in
  (set_deliver_partial): a chunk-aligned frame that ages out with holes is handed
  over as Frame{complete:false} — received shards at their exact offsets, missing
  ranges zero-filled — instead of being dropped. Partials age out on a tight 30ms
  fuse (PARTIAL_WINDOW_NS) instead of the 120ms loss window: each frame is
  independently decodable, so an ancient partial has no value in a live stream.
  Newest-wins. A partial still counts as dropped for loss reporting.

Client (PyroWave decode):
- The session opts in when codec == PyroWave. The decoder walks the AU
  window-by-window, skipping zero (missing) windows and reassembling FRAG chains,
  then decodes whatever survived. A newest-decoded-index guard drops partials the
  pump has already moved past (no time-travel present).

Also fixes a redundant-closure clippy nit in the PyroWave planar-present path.

Validated on an RTX 5070 Ti under 2% netem loss with FEC pinned off: 60fps
sustained entirely via partials, e2e 43ms p50 (146ms before the fuse) vs 23ms
lossless, no keyframe-recovery chatter. Tests green: core 149, host 310 + the
GPU-gated encoder smoke (framed-window walk + FRAG reassembly + upstream
round-trip), client 26; clippy clean on the pyrowave feature combos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:14:24 +02:00
enricobuehler 12148243bd feat: PyroWave Phase 3 — pinned rate, all-intra silencing, opt-in UI, notices, docs
ci / docs-site (push) Successful in 1m0s
ci / web (push) Successful in 2m13s
arch / build-publish (push) Failing after 6m17s
decky / build-publish (push) Successful in 18s
android / android (push) Failing after 6m43s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 53s
ci / bench (push) Successful in 5m6s
ci / rust (push) Failing after 9m30s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Failing after 4m50s
deb / build-publish (push) Failing after 7m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 6m2s
windows-host / package (push) Failing after 3m4s
apple / swift (push) Failing after 1m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m1s
release / apple (push) Failing after 1m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 2m15s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 4m36s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 4m43s
apple / screenshots (push) Has been skipped
plan §4.6 + Phase 3 productization:

- Pinned bitrate: an Automatic client (bitrate 0) on a PyroWave session
  resolves to the codec's ~1.6 bpp operating point for the mode (≈200
  Mbps at 1080p60) instead of the 20 Mbps H.26x default; explicit rates
  are honored. Mid-stream SetBitrate retargets are refused with the
  pinned rate acked (guards old/foreign clients), and the client-side
  AIMD controller + startup capacity probe stay off for the codec — no
  rate descent into wavelet mush, no climb probe whose VBV reasoning
  doesn't apply to hard per-frame CBR. Unit-tested.

- All-intra silencing: the data plane drops drained keyframe/RFI
  requests on PyroWave sessions (the next frame IS the recovery), so
  the forced-IDR cooldown, RFI attempt, and storm coalescing never run.

- Opt-in UI: 'PyroWave (wired LAN)' joins the console's Video-codec
  cycler; trust::Settings maps it to CODEC_PYROWAVE. Safe everywhere by
  the negotiation contract — an un-advertised preference falls back
  through the ladder.

- FEC: decision recorded — adaptive FEC (10% start, loss-report driven)
  stays as-is for the MVP opaque-AU mode; the FEC≈0 policy belongs to
  the Phase-4 datagram-aligned mode.

- THIRD-PARTY-NOTICES: the generator now lists third-party trees
  vendored inside first-party crates (pyrowave, Granite subset, volk,
  Vulkan-Headers) with their full license texts; file regenerated.

- docs-site: 'PyroWave (wired-LAN codec)' page — what it is, the
  bandwidth table, how to enable it, current limits.

Validated on .21: 309 host + 148 core + 26 client tests green,
console-ui clean, both feature configs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:45:49 +02:00
enricobuehler 8dc5d672e2 feat(host): PyroWave capture advertises the Vulkan device's dmabuf modifiers
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 51s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
ci / bench (push) Successful in 6m35s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m57s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 12m46s
arch / build-publish (push) Successful in 15m29s
deb / build-publish (push) Successful in 16m21s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m33s
windows-host / package (push) Failing after 12m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m22s
ci / rust (push) Successful in 28m2s
apple / swift (push) Successful in 4m45s
apple / screenshots (push) Successful in 19m36s
The pyrowave passthrough rode VAAPI's LINEAR-only modifier policy, which
starves it on Mutter+NVIDIA (tiled-only allocations → the compositor
declines the offer → CPU capture fallback). The encoder imports through
VK_EXT_image_drm_format_modifier, not libva, so the capture now extends
the advertisement with every single-memory-plane modifier the PyroWave
device samples from (probed via DrmFormatModifierPropertiesListEXT with
the same device selection as the encoder).

Live on .21 (Mutter+NVIDIA, RTX 5070 Ti): 7 modifiers advertised, the
compositor negotiated block-linear (216172782120099861), no CPU
downgrade, and the encoder's per-buffer import cache populated exactly
as designed (8 PipeWire pool buffers imported once, silent reuse after).
Zero-copy session numbers: static 60 fps, e2e 2.9-3.0 ms p50 (p95 3.4),
host stage 1.6 ms; full-window motion 60 fps at ~80 Mb/s all-intra,
decode ~1 ms. Also neutralizes the VAAPI-specific wording in the
passthrough hand-off log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:30:59 +02:00
enricobuehler 719b1ef403 fix(core): let CODEC_PYROWAVE survive the Welcome decode whitelist
ci / web (push) Successful in 1m8s
ci / docs-site (push) Successful in 1m12s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 37s
apple / swift (push) Successful in 2m34s
ci / bench (push) Successful in 6m53s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10m4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9m10s
arch / build-publish (push) Successful in 13m42s
docker / deploy-docs (push) Successful in 21s
android / android (push) Successful in 14m48s
deb / build-publish (push) Successful in 14m53s
windows-host / package (push) Successful in 15m18s
flatpak / build-publish (push) Failing after 8m14s
ci / rust (push) Successful in 18m28s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m27s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m16s
release / apple (push) Successful in 24m45s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m21s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m4s
apple / screenshots (push) Successful in 19m29s
Found by the first live session on .21: the host negotiated PyroWave
and put codec=8 on the wire, but Welcome::decode's codec whitelist
(H264/AV1, else HEVC — the corrupt-byte guard) folded it to HEVC, so
the client opened an FFmpeg HEVC decoder against wavelet AUs. Roundtrip
test now pins the pyrowave byte (and that a genuinely unknown future
bit still folds to the HEVC default).

With the fix the Phase-2 exit session runs END TO END on .21
(host + session client on one box, host capturing the GNOME virtual
display, client presenting into a headless weston):
  negotiated codec=PyroWave (adv 0x0f) → PyroWave encoder (CPU-capture
  path — this box's Mutter+NVIDIA rejects the LINEAR-dmabuf offer) →
  wire → PyroWave decoder on the presenter's device → planar CSC.
  Static desktop: stable 60 fps, e2e 2.1-4.1 ms p50 (p95 <= 6 ms),
  decode 0.2-0.6 ms, vs HEVC/NVENC-direct baseline 2.1 ms — parity at
  idle. Full-window motion: 60 fps at ~80 Mb/s all-intra (HEVC ~7),
  decode still sub-ms, zero decode errors or keyframe-request chatter
  across every run. Deeper motion/loss characterization needs a
  dmabuf-accepting host box (this one is capped by the CPU capture
  path).

Also retires the stale "no shipping client decodes this" wording in
the host encoder/dispatch logs — the negotiation exists now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:09:39 +02:00
enricobuehler f77eec1299 feat(client): PyroWave planar present path + Linux NVENC match-arm fix (Phase 2b, part 2)
android / android (push) Failing after 45s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 56s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
arch / build-publish (push) Failing after 4m15s
ci / rust (push) Failing after 3m59s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
deb / build-publish (push) Failing after 4m7s
ci / bench (push) Successful in 5m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m46s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m58s
flatpak / build-publish (push) Failing after 8m3s
docker / deploy-docs (push) Successful in 22s
windows-host / package (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m31s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m35s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 21m40s
The arch package job (--features nvenc) tripped the same class of
Codec::PyroWave non-exhaustive matches as windows-host had, in
nvenc_cuda.rs (6 sites) — dispatch-guarded unreachable!() arms, plus
the vk_util-extraction leftover unused imports in vulkan_video.rs.
All Linux host feature combos (none / pyrowave / nvenc,vulkan-encode /
all three) now compile clean on .21.

Presenter: planar_csc.frag (+ committed .spv) — the 3-plane variant of
nv12_csc.frag (separate Cb/Cr R8 planes, same push-constant CSC-row
contract, siting correction self-disables at full-res chroma).
CscPass grows a shared builder + new_planar()/bind_planes_planar()
(GENERAL-layout descriptors — pyrowave planes stay GENERAL); the Vk
presenter builds the planar pass when the device passed the pyrowave
probe, FrameInput::PyroWave rides present_frame (no acquire barrier
needed: the decoder fence-completed and barriered the planes on the
same queue), and run.rs presents it with no demote rung (only device
loss ends the session).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:42:15 +02:00
enricobuehler 49ba1cd11b fix(host): cover Codec::PyroWave in the Windows NVENC/AMF match arms
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m45s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 31s
arch / build-publish (push) Failing after 8m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m57s
android / android (push) Successful in 12m13s
ci / rust (push) Failing after 13m5s
deb / build-publish (push) Failing after 7m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m30s
windows-host / package (push) Successful in 14m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m37s
docker / deploy-docs (push) Successful in 25s
apple / swift (push) Successful in 4m51s
apple / screenshots (push) Successful in 20m29s
The nine non-exhaustive matches windows-host CI tripped on (run 9917) —
all inside encoder objects a PyroWave session can never open (the
open_video dispatch routes PyroWave to its own backend on Linux and
bails on Windows), so the arms are dispatch-guarded unreachable!().
Verified: cargo check -p punktfunk-host --features nvenc,amf-qsv
--release green on the windows-amd64 runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:20:56 +02:00
enricobuehler e71cb9b7bd feat(core,host): CODEC_PYROWAVE negotiation — opt-in only, host dispatch wired
audit / bun-audit (push) Successful in 11s
windows-host / package (push) Failing after 48s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
android / android (push) Failing after 58s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
audit / cargo-audit (push) Successful in 2m15s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m18s
ci / bench (push) Successful in 6m37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Failing after 8m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m59s
arch / build-publish (push) Failing after 12m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m54s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m49s
flatpak / build-publish (push) Failing after 8m16s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
ci / rust (push) Failing after 19m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m56s
docker / deploy-docs (push) Successful in 21s
apple / swift (push) Successful in 4m53s
release / apple (push) Successful in 25m50s
apple / screenshots (push) Successful in 20m55s
Phase 2a+2c of design/pyrowave-codec-plan.md.

Core: CODEC_PYROWAVE = 0x08 on Hello::video_codecs/Welcome::codec.
Deliberately absent from resolve_codec's precedence ladder (plan §3 —
a 100-400 Mbps codec must never win a negotiation by mere mutual
support): reachable exclusively through the client's explicit
preferred_codec. Invariant tests cover never-auto-selected (even as the
only shared codec), preferred-path selection, and graceful fallback.
ABI mirror PUNKTFUNK_CODEC_PYROWAVE + lockstep assert for the
Apple/Android embedders.

Host: Codec::PyroWave variant threaded through the wire mappings; a
negotiated PyroWave session routes straight to the backend ahead of the
PUNKTFUNK_ENCODER pref dispatch (which stays a lab override). The
advertisement bit rides host_wire_caps only when the capture side would
actually deliver ingestible frames — linux_zero_copy_is_vaapi(), i.e.
AMD/Intel auto or an explicit operator pref on NVIDIA; per-session
raw-dmabuf OutputFormat plumbing is recorded as the Phase-3 item. The
libavcodec name helpers are dispatch-guarded unreachable; the web
console gains ApiCodec::PyroWave (api/openapi.json regenerated).

Validated on .21: 308 host tests green with and without the feature,
145 core tests green with quic, clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:14:04 +02:00
enricobuehler 767f028bdf feat(host): PyroWave encoder — Phase 1 of the LAN low-latency codec plan
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>
2026-07-15 00:58:02 +02:00
enricobuehler d2b4e3d71c fix(host): warn loudly when a CUDA session runs a build without direct-SDK NVENC
android / android (push) Has been cancelled
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Successful in 16s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / deploy-docs (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
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>
2026-07-15 00:03:26 +02:00
enricobuehler 9d67dc18aa perf(core): two-lane AES-GCM seal for large frames + send-thread stage split
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 1m29s
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Successful in 17s
docker / deploy-docs (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
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>
2026-07-14 23:47:33 +02:00
enricobuehler f4f6c5556f perf(core): FEC encoder reuse — cached codecs + pooled parity, no per-block setup
ci / docs-site (push) Successful in 1m0s
ci / web (push) Successful in 1m0s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m4s
windows-host / package (push) Successful in 9m3s
flatpak / build-publish (push) Failing after 8m2s
docker / deploy-docs (push) Successful in 24s
deb / build-publish (push) Successful in 12m19s
android / android (push) Successful in 12m46s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m22s
arch / build-publish (push) Successful in 14m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m34s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m57s
ci / rust (push) Successful in 23m21s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m57s
release / apple (push) Has been cancelled
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
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>
2026-07-14 23:19:21 +02:00
enricobuehler 5c7e0afa99 fix(host): Linux virtual-pad feedback access — hidraw udev rules, per-pad DS MAC, SET_REPORT acks
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m3s
ci / bench (push) Successful in 5m49s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / deploy-docs (push) Successful in 23s
arch / build-publish (push) Successful in 11m11s
android / android (push) Successful in 14m24s
deb / build-publish (push) Successful in 11m55s
ci / rust (push) Successful in 17m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m54s
windows-host / package (push) Successful in 15m11s
apple / swift (push) Successful in 4m43s
apple / screenshots (push) Successful in 21m4s
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>
2026-07-14 21:47:13 +02:00
enricobuehler 5a384fe788 feat(host): pace-aware send chunking — high-rate frames pace honestly instead of blasting
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 23s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 6m0s
flatpak / build-publish (push) Failing after 8m9s
docker / deploy-docs (push) Successful in 25s
deb / build-publish (push) Successful in 12m48s
android / android (push) Successful in 13m11s
arch / build-publish (push) Successful in 15m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m32s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m29s
ci / rust (push) Successful in 23m9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m29s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m35s
windows-host / package (push) Successful in 14m25s
apple / swift (push) Successful in 5m13s
release / apple (push) Successful in 26m56s
apple / screenshots (push) Successful in 20m2s
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>
2026-07-14 21:38:06 +02:00
enricobuehler a87b279c2b test(host): Windows on-hardware NVENC reconfigure smoke — 20→60→10 Mbps in place, zero IDRs
ci / web (push) Successful in 1m16s
ci / docs-site (push) Successful in 1m37s
arch / build-publish (push) Successful in 10m16s
decky / build-publish (push) Successful in 36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
ci / bench (push) Successful in 5m11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m2s
android / android (push) Successful in 15m11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m51s
docker / deploy-docs (push) Successful in 16s
deb / build-publish (push) Successful in 12m48s
ci / rust (push) Successful in 16m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m32s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m47s
apple / swift (push) Successful in 4m30s
windows-host / package (push) Successful in 14m43s
apple / screenshots (push) Successful in 21m55s
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>
2026-07-14 20:20:29 +02:00
enricobuehler 9bf72cdfb5 fix(host): forward reconfigure_bitrate through TrackedEncoder + probe --rebitrate validator
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m5s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 13m33s
android / android (push) Successful in 14m55s
ci / bench (push) Successful in 5m19s
ci / rust (push) Successful in 16m59s
flatpak / build-publish (push) Failing after 8m7s
apple / swift (push) Successful in 4m30s
deb / build-publish (push) Successful in 11m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m31s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m8s
windows-host / package (push) Successful in 8m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m46s
release / apple (push) Successful in 24m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m12s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m56s
apple / screenshots (push) Successful in 21m24s
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>
2026-07-14 20:06:56 +02:00
enricobuehler a1af916e38 feat(host): in-place encoder rate reconfigure — ABR steps no longer cost an IDR
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m8s
ci / bench (push) Successful in 5m25s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
arch / build-publish (push) Successful in 11m11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
android / android (push) Successful in 14m53s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m24s
deb / build-publish (push) Successful in 11m50s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
ci / rust (push) Successful in 23m4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m54s
docker / deploy-docs (push) Successful in 22s
apple / swift (push) Successful in 4m31s
windows-host / package (push) Successful in 14m46s
apple / screenshots (push) Successful in 18m26s
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>
2026-07-14 19:53:18 +02:00
enricobuehler 9b7fc127ef feat(core): Automatic bitrate scales to measured link capacity — probe ceiling + slow start
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 45s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 6m1s
flatpak / build-publish (push) Failing after 8m11s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 12m30s
windows-host / package (push) Successful in 14m50s
arch / build-publish (push) Successful in 15m1s
android / android (push) Successful in 15m47s
ci / rust (push) Successful in 17m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m17s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m4s
release / apple (push) Successful in 25m7s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m4s
apple / screenshots (push) Successful in 18m44s
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>
2026-07-14 19:28:11 +02:00
enricobuehler 85513d1697 fix(host/linux): headless gamescope must not inherit a desktop DISPLAY/WAYLAND_DISPLAY
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>
2026-07-14 19:07:41 +02:00
enricobuehler 840e5d590e fix(host/linux): free a desktop-session Steam before a dedicated gamescope launch
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>
2026-07-14 16:32:29 +02:00
enricobuehler f3b6ccaa7f fix(gamepad/windows): Steam-accepted Deck unit serial un-mangles the controller name
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>
2026-07-14 16:31:07 +02:00
enricobuehler d8e8529cd7 feat(gamepad): Windows Steam Deck backend — Steam-Input-promoted UMDF virtual Deck
windows-drivers / probe-and-proto (push) Successful in 18s
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / web (push) Successful in 51s
windows-drivers / driver-build (push) Successful in 1m51s
ci / docs-site (push) Successful in 54s
ci / bench (push) Successful in 5m20s
arch / build-publish (push) Successful in 13m32s
android / android (push) Successful in 15m7s
ci / rust (push) Successful in 21m11s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 20m53s
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>
2026-07-14 12:36:26 +02:00
enricobuehler 4201851c7f fix(fmt) + feat(gamepad): CI-matching rustfmt everywhere, enforced by repo git hooks; N4 spike flips to GO via MI_02
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
arch / build-publish (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
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>
2026-07-14 12:20:33 +02:00
enricobuehler ea2e3578e2 feat(gamepad): dualsense-windows-test grows --edge (drives device_type 2, cycles R4/L4)
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>
2026-07-14 11:50:06 +02:00
enricobuehler 8d8168b0e0 feat(gamepad): N4 spike kit — software-devnode Steam Deck probe for Windows
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>
2026-07-14 11:44:59 +02:00
enricobuehler 8c854e0a19 feat(gamepad): classic Steam Controller backend — Linux UHID via hid-steam (N3)
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>
2026-07-14 11:15:54 +02:00
enricobuehler 70a74b0d7c feat(gamepad): Switch Pro backend — Linux UHID via hid-nintendo (N2)
A virtual Pro Controller (057E:2009, BUS_USB, verbatim 203-byte USB
descriptor triple-cross-checked from real-device captures) bound by
hid-nintendo (>= 5.16): Nintendo-family client pads get correct glyphs +
POSITIONAL layout (wire south/east/west/north -> Switch B/A/Y/X, so the
physical-position <-> glyph relationship survives), live gyro/accel, and
HD-rumble feedback — instead of folding to Xbox360 (mirrored A/B + X/Y,
no motion).

- switch_proto: report-0x30/0x21/0x81 codec + the entire canned probe
  conversation, pinned line-by-line against hid-nintendo.c: 0x80-family
  USB acks, device info (type 0x03 + per-pad MAC), SPI-flash calibration
  blobs (user magics ABSENT -> factory path; sticks 2048 +/- 1400 with
  the left/right byte-order difference; IMU offsets 0 + the driver's own
  default scales so raw units pass 1:1), rumble amplitude decode through
  the driver's inverted joycon_rumble_amplitudes table, player lights ->
  0xCD PlayerLeds. 11 new pin tests.
- switch_pro: UHID backend answering the probe from the manager's
  service pass; SwitchProManager = UhidManager<SwitchProProto> (the 8 ms
  heartbeat doubles as the steady 0x30 stream the driver's post-probe
  rate limiter wants). switchpro-test CLI smoke.
- Router/fold: SwitchPro arms; pick_gamepad SwitchPro -> itself on Linux;
  degrade_if_no_uhid covers it. SDL picker: NintendoSwitchPro + JoyconPair
  declare SwitchPro.

Headless-validated on .21 (hid-nintendo 7.1): probe completes ('using
factory cal' for sticks + IMU, player-1 LED round-trips to the 0xCD
plane), gamepad + IMU input devices created, and an evdev capture pins
the positional swap (wire A/B -> BTN_SOUTH/BTN_EAST) + full-range stick
scaling. .21 clippy -D warnings + 303/0 tests; .133 clippy -D warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:05:28 +02:00
enricobuehler 1830e095f8 feat(gamepad): DualSense Edge backend — Linux UHID + Windows UMDF (N1)
The plain-DualSense transport + report codec under the Edge USB identity
(054C:0DF2, verbatim 389-byte real-device descriptor cross-checked against
the raw usbmon capture + hhd's production virtual Edge), so the wire back
grips (BTN_PADDLE1..4: Deck L4/L5/R4/R5, Elite P1-P4) land on the Edge's
NATIVE buttons[2] bits instead of the fold/drop policy: PADDLE1/2 -> the
right/left back buttons, PADDLE3/4 -> the right/left Fn buttons (kernel
BTN_TRIGGER_HAPPY1..4 on >= 7.2; SDL/Steam read hidraw on any kernel).

- proto: Edge descriptor + btn2 bits + edge_paddle_bits(), pinned against
  hid-playstation DS_EDGE_BUTTONS_* and SDL_hidapi_ps5 (tests).
- Linux: DsUhidIdentity parameterizes the UHID create; DsEdgeLinuxProto /
  DualSenseEdgeManager. Headless-validated on .21 (7.1): driver=playstation
  binds 0DF2, all 4 input devices created, probe lightbar/player-LED
  feedback round-trips; dualsense-test grew --edge (cycles all 4 paddles).
- Windows: UMDF driver serves device_type=2 (Edge descriptor/attrs/strings,
  DS feature blobs); WinDsIdentity parameterizes the SwDevice profile +
  devtype stamp; DsEdgeWinProto / DualSenseEdgeWindowsManager; INF gains
  pf_dualsenseedge. Driver change => resign + reinstall before on-glass.
- Router: DualSenseEdge arms in route_handle/apply_rich/pump/heartbeat;
  pick_gamepad folds Edge -> itself on linux||windows; degrade_if_no_uhid
  covers it.
- Client (SDL): 054C:0DF2 declares DualSenseEdge (no distinct SDL type);
  Edge physical pads take the raw DS5 effects path; console-UI glyphs =
  Shapes. Apple/Android pickers follow separately.

Verified: .21 clippy -D warnings + 292/0 host tests + on-box UHID bind
smoke; .133 clippy pending in this push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:49:31 +02:00
enricobuehler 45bde370e2 feat(gamepad): GamepadPref wire bytes for DualSense Edge (7) + Switch Pro (8)
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>
2026-07-14 10:20:24 +02:00
enricobuehler 650acda334 chore(inject): post-extraction sweep (3.3)
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>
2026-07-14 02:04:51 +02:00
enricobuehler 89aa52bc58 refactor(inject): uinput + XUSB managers onto PadSlots (3.3)
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>
2026-07-14 01:57:00 +02:00
enricobuehler 384fc30833 refactor(inject/linux/steam_controller): convert to UhidManager<SteamProto> (3.3)
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>
2026-07-14 01:41:21 +02:00
enricobuehler 365d4bb8f1 refactor(inject/linux/dualshock4): convert to UhidManager<Ds4LinuxProto> (3.3)
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>
2026-07-14 01:38:45 +02:00
enricobuehler f1efd3091e refactor(inject/windows/dualshock4): convert to UhidManager<Ds4WinProto> (3.3)
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>
2026-07-14 01:36:45 +02:00
enricobuehler 446818eea6 refactor(inject/windows/dualsense): convert to UhidManager<DsWinProto> (3.3)
DualSenseWindowsManager becomes a pub type alias of
UhidManager<DsWinProto>; the proto supplies the UMDF sealed-channel open
(+ success log), the DsState mappers (identical to linux/dualsense.rs,
paddle fold included), and the section feedback poll. Lifecycle, dedup,
and heartbeat come from the shared skeleton — behavior-identical, same
log lines (LABEL DualSense/Windows + the driver-install hint).

DsWinPad goes pub (it appears as type Pad in the impl of the public
PadProto trait — E0446 otherwise; the Linux pads were already pub).

Verified on the Windows CI VM .133 (same pinned 1.96.0 MSVC toolchain +
Public-path FFmpeg/LLVM the runner uses): cargo clippy -p punktfunk-host
--all-targets -- -D warnings EXITCODE 0 at the DS4-conversion tip
(.173 was down; .133 carries the identical toolchain).

Part of G12/3.3 (§3a.4 commit 5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:36:45 +02:00
enricobuehler 4d6c2394dc refactor(inject/linux/dualsense): convert to UhidManager<DsLinuxProto> (3.3)
The first backend onto the shared skeleton: DualSenseManager becomes
pub type DualSenseManager = UhidManager<DsLinuxProto>, where DsLinuxProto
supplies only the protocol half (UHID open + success log, DsState
neutral/merge/apply_rich with the paddle fold, best-effort write, the
GET_REPORT-answering service pass). handle/apply_rich/heartbeat/pump and
the unplug sweep now come from uhid_manager — behavior-identical
(same log lines, same dedup + reset semantics), zero Pads-router edits.

Verified on .21: clippy --all-targets -D warnings clean; full suite 290
pass / 0 fail.

Part of G12/3.3 (§3a.4 commit 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:10:42 +02:00
enricobuehler 2bea02b0ea feat(inject): generic PadProto + UhidManager<B> stateful manager (3.3 layer 2)
The shared skeleton of the five stateful UHID/UMDF managers (Linux
DualSense / DualShock 4 / Steam Deck, Windows DualSense / DualShock 4),
written once over PadSlots: event routing with the unplug sweep and
was-the-unplug early return, the merge-preserving frame fold, rich-input
application, the silence heartbeat (with a backend force hook for the
Steam mode-entry pulse), and the feedback pump with rumble dedup +
HidoutDedup. A backend supplies only its per-controller half via
PadProto: open / neutral / merge_frame / apply_rich / write_state /
service — exactly where the real protocol differences live.

Method surface (new/handle/apply_rich/pump/heartbeat) matches what the
punktfunk1.rs Pads router already drives, so each backend will convert
as a pub type alias with zero router edits.

Additive only — no backend converted yet. 8 mock-backend tests make the
manager lifecycle unit-testable for the first time; G2 (rich fields
survive a button-only frame) and G10 (Arrival eager-creates) are now
generic regression tests, plus removal-frame no-recreate, absent-pad
rich drop, create-backoff state tracking, rumble/hidout dedup + re-arm
on recreate, and heartbeat gap/force semantics.

Verified on .21: clippy --all-targets -D warnings clean; suite 293
pass / 0 fail (285 prior + 8 new).

Part of G12/3.3 (gamepad-review-cleanup.md §3a.3, commit 3 of the §3a.4
sequence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:08:25 +02:00
enricobuehler 528a51d75c feat(inject): shared PadSlots<P> slot table + lifecycle (3.3 layer 1)
The Vec<Option<Pad>> slot table, active_mask unplug sweep, and PadGate-
checked create that all seven backend managers copy-paste, extracted into
one unit-tested inject/pad_slots.rs (cfg any(linux,windows), like
pad_gate). sweep() returns the swept indices as a bitmask and ensure()
returns fresh-create, so managers reset their per-index sibling state
(state / last_rumble / dedup / clocks) without closure gymnastics.
Lifecycle log lines are label/device/hint-parameterized to stay
byte-identical per backend; open() keeps the success line (it knows the
transport detail).

Additive only — no manager converted yet; first unit coverage for the
sweep/create lifecycle (5 tests: freshness, sweep-once semantics, gate
integration, recreate, pump iteration).

Verified on .21: clippy --all-targets -D warnings clean; suite 285
pass / 0 fail (280 prior + 5 new).

Part of G12/3.3 (gamepad-review-cleanup.md §3a.3, commit 2 of the §3a.4
sequence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:04:22 +02:00
enricobuehler b597bb74bd fix(inject/linux/ds4): fold the Linux DS4 backend onto the shared proto codec (3.3.0)
The Linux DualShock 4 backend missed the G2-era shared-mapping work and
drifted from dualshock4_proto three ways, leaving two user-visible gaps
on the DS4 kind (Windows, written later against the proto, is correct):

- its serialize_state duplicated the proto's byte-for-byte EXCEPT byte 7:
  raw st.buttons[2] instead of buttons2_with_click(), so a rich-plane pad
  click never reached the report;
- its inline apply_rich never set touch_click and dropped the Steam LEFT
  pad entirely (surface 1 skipped), where the shared
  dualsense_proto::DsState::apply_rich splits the one touchpad left/right;
- handle() didn't preserve touch_click across button-only frames.

Net effect: Deck client -> Linux host on the DS4 kind = pad clicks and
the left pad dead.

Delete the local serialize_state/parse_ds4_output/Ds4Feedback/pack_touch
and touch-dim consts in favor of dualshock4_proto (dropping the proto's
keep-in-sync FIXME), route rich events through the shared
DsState::apply_rich, and preserve touch_click in the frame merge exactly
like the other three DS-family managers. The proto's serialize_offsets
test gains a touch_click case pinning byte 7 bit 1.

Verified on .21: cargo clippy -p punktfunk-host --all-targets -D warnings
clean; full suite 277 pass / 0 fail.

Pre-step 3.3.0 of the G12 skeleton extraction (gamepad-review-cleanup.md
§3a.2) — the behavior fix lands before the mechanical dedup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:01:37 +02:00
enricobuehler 94802795e7 Merge branch 'gamepad-apple-cleanup': cross-client + host gamepad review cleanup (G1–G25)
audit / bun-audit (push) Successful in 13s
ci / docs-site (push) Successful in 49s
ci / web (push) Successful in 53s
decky / build-publish (push) Successful in 21s
audit / cargo-audit (push) Successful in 2m27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
apple / swift (push) Successful in 4m22s
ci / bench (push) Successful in 5m54s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
windows-host / package (push) Successful in 9m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9m58s
flatpak / build-publish (push) Successful in 6m18s
docker / deploy-docs (push) Successful in 27s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m14s
arch / build-publish (push) Successful in 16m1s
android / android (push) Successful in 16m36s
deb / build-publish (push) Successful in 16m58s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m46s
ci / rust (push) Successful in 22m42s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m19s
release / apple (push) Successful in 20m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m49s
apple / screenshots (push) Successful in 18m55s
48-finding cross-client + host gamepad audit (2026-07-13). Apple/Android/SDL-core
capture + feedback and the Linux/Windows host injectors: held-guide release, the
permanent broken-latch cliff (PadGate), Steam Deck trackpad clicks, DualSense mute,
Windows DS/DS4 paddle fold, uinput button re-sync, gamestream BTN_* dedup, the dead
Windows shell fork, legacy-Deck rumble ceiling, XUSB arrival, ARM64 fences, the
truncate-everywhere value convention, and more. See
punktfunk-planning/design/gamepad-review-cleanup.md.
2026-07-13 22:29:41 +02:00
enricobuehler 1af11cc64d fix(inject/host/windows): order the pad change-detect fields with Release/Acquire (G21)
The XUSB `packet` publish and the XUSB `rumble_seq` / DualSense `out_seq` reads
used plain unaligned accesses with no fence, so a driver could observe a bumped
change-detect field over a torn body on a weakly-ordered core (ARM64). Publish
`packet` via a Release AtomicU32 store behind a Release fence, and Acquire-load
the seq fields, mirroring the gamepad_raii PadChannel seq-fence precedent. The
DualSense input report embeds its seq mid-report with no driver-gated
change-detect field, so it gets a Release fence after the copy and a documented
residual (a per-frame input generation is deferred). No-op on x86-TSO.

Verified: Windows .173 `cargo clippy -p punktfunk-host --all-targets -- -D warnings` (green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:05:13 +02:00
enricobuehler 2f214532d9 fix(inject/host/windows): eager-create the XUSB pad on Arrival + refresh last_active (G10)
The XUSB manager's `handle` dropped `GamepadEvent::Arrival` via a `let else`, so
the GameStream path never created the pad until the first `State` and missed the
first XInput poll. Match on the event and `ensure` eagerly on Arrival, mirroring
the DualSense backend. Also refresh `last_active` on create and unplug so a
freshly-created pad's residual-rumble idle clock starts fresh rather than
inheriting a stale Instant (which could force off a legitimate rumble at once).

Verified: Windows .173 `cargo clippy -p punktfunk-host --all-targets -- -D warnings` (green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:05:13 +02:00
enricobuehler 31bc863084 fix(inject/host/windows): free the per-section security descriptor (G19)
`sddl_sa` leaked the `LocalAlloc`'d PSECURITY_DESCRIPTOR that
ConvertStringSecurityDescriptorToSecurityDescriptorW returns, once per DATA
section and once per bootstrap mailbox create (amplifiable under pad-flap via
create_named's squat-retry loop). Wrap it in a `SecAttr` RAII owner that
`LocalFree`s on drop; it outlives every CreateFileMappingW (the section copies
the security info at create time), and create_named builds one and reuses it
across retries instead of re-allocating.

Verified: Windows .173 `cargo clippy -p punktfunk-host --all-targets -- -D warnings`
(green) -- confirms the LocalFree/HLOCAL signature at the pinned windows-rs rev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:05:13 +02:00
enricobuehler e5166c6e6e fix(host/steam): load vhci_hcd at boot on sysext hosts so the Deck pad is Steam-Input-promotable
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
decky / build-publish (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 4m25s
ci / bench (push) Successful in 6m50s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m49s
docker / deploy-docs (push) Successful in 26s
windows-host / package (push) Successful in 8m40s
arch / build-publish (push) Successful in 11m22s
deb / build-publish (push) Successful in 12m27s
android / android (push) Successful in 16m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m19s
apple / screenshots (push) Successful in 19m19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m14s
ci / rust (push) Successful in 25m14s
The virtual Steam Deck pad only appears in the host's Game Mode (and is
navigable) when it arrives as a real USB device via the usbip/vhci_hcd
transport — Steam Input won't promote the UHID hid-steam fallback
(Interface: -1). The host runs as an unprivileged --user service, so it
cannot modprobe vhci_hcd itself; the module must be loaded at boot and the
vhci attach/detach sysfs files chgrp'd to the `input` group by the udev
rule.

Packaging ships modules-load.d/punktfunk.conf + 60-punktfunk.rules under
the sysext's /usr/lib, but a systemd-sysext image MERGES after
systemd-modules-load and early udev have already run, so on a plain reboot
of a sysext host (e.g. Bazzite) those files are read too late: vhci_hcd is
never loaded, usbip fails, and the pad silently degrades to non-promoted
UHID — the controller vanishes from Game Mode. (deb/arch/rpm are
unaffected: real /usr is present at early boot.)

Fix: sysext post_merge now mirrors BOTH files into real /etc (read at the
normal early-boot time, shadowing the /usr copies by filename; refreshed
every merge since neither is user config), then reloads udev, modprobes
vhci-hcd, and re-triggers the vhci platform device for the live session.
Also raise the UHID-fallback log INFO->WARN with an actionable hint.

Verified on the .41 sysext host: after the /etc mirror, unloading vhci_hcd
and restarting systemd-modules-load (the real reader of /etc/modules-load.d)
reloads the module; a udev coldplug trigger makes attach/detach root:input
0660; the unprivileged host user can then write attach — the exact working
precondition for the usbip transport, now durable across reboot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:41:51 +02:00
enricobuehler 59fc820226 perf(inject/host): dedup the DualSense HID-output feedback plane (G17)
A game's DualSense output report bundles rumble + lightbar + player-LEDs
+ adaptive-triggers into one report, so a pad that is merely rumbling
re-sends its unchanged lightbar / LED / trigger state on every output
report. The managers already dedup rumble, but forwarded every rich
`HidOutput` event verbatim — flooding the 0xCD feedback plane to the
client during continuous rumble.

Add a shared `HidoutDedup` (dualsense_proto, used by both the Linux UHID
and Windows UMDF managers) that forwards Led/PlayerLeds/Trigger only on a
value change (per side for the two triggers) and always forwards one-shot
TrackpadHaptic pulses — mirroring the rumble dedup two lines above and the
DS4 backend's lightbar dedup. Reset per pad on create/unplug.

Verified on Linux .21 (clippy -D warnings clean, new HidoutDedup unit
test + full suite green); Windows .173 with the rest of Phase 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:14:28 +02:00