Commit Graph

163 Commits

Author SHA1 Message Date
enricobuehler de7b54d4e0 fix(core): remaining 15 sweep lows — client, clipboard, C ABI (v10), FEC, GSO, GTK
ci / docs-site (push) Successful in 58s
ci / web (push) Successful in 1m11s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 29s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
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
deb / build-publish-host (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (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 (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
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-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 / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
Second and final batch from the 2026-07-20 core-sweep lows:

client:
- connect() timeout now sets `quit` before shutdown, so a handshake that
  completes after the deadline closes with QUIT_CLOSE_CODE instead of
  leaving the host lingering (virtual display up) for a reconnect that
  never comes.
- probe_result: saturating_add on the wire-supplied wire_packets +
  send_dropped counters (debug-build overflow panic / release wrap).
- the standing-latency bleed no longer sets flush_in_window: that flag
  is the ABR's SEVERE (×0.7) verdict, and the bleed fires only on
  provably loss-free windows the controller itself scores as fine.

clipboard:
- fetch_cancels pruned on every new fetch (was: one dead oneshot per
  paste for the session).
- serve chunks gated on a parked waiter + capped at CLIP_FETCH_CAP with
  an Error event (was: unbounded silent accumulation under any req_id).
- serve_inbound park bounded by FETCH_STALL_SECS + send.stopped() (was:
  an unanswered FetchRequest parked the task, waiter, and bi-stream
  forever; ~100 of them exhaust the connection's bidi budget).

C ABI (ABI_VERSION 9 → 10, header regenerated, additive only):
- new punktfunk_connection_clock_offset_now_ns — the LIVE re-synced
  offset (Swift/Kotlin latency math read the frozen connect-time value
  ~40ms wrong after a wall-clock step).
- to_config: checked u64→usize narrowing of max_frame_bytes (32-bit
  armeabi truncated >4GiB to a plausible residue).
- host_poll_input: no &mut held across the embedder callback (re-entry
  aliased it — UB under noalias); mid-drain callback clears now stick.
- next_audio_pcm: DTX (empty) payloads skipped — decode synthesized
  120ms of concealment per 5ms slot and grew the playout ring forever.
- next_clipboard releases the parked payload on an empty poll (a one-off
  50MiB paste stayed resident all session).
- frames_dropped / wants_decode_latency write their documented 0/false
  defaults before the NULL-handle check.
- gamepad constant docs match pick_gamepad() reality (DualSenseEdge/
  SwitchPro landed; DualSense/DS4 honored on Windows UMDF too).

FEC:
- gf8 reconstruct/reconstruct_into reuse the (k,m) codec cache like
  encode_into (was: fresh 230×200 generator + decode inversion per
  lossy block on the pump thread).
- vendored fec-rs: the 8 safe wrap_mul_slice shims assert equal lengths
  (x86 SIMD callees bound stores on input.len() — safe-code OOB write);
  ReconstructShard's safety contract gains the len()==get().len()
  clause and reconstruct_internal sizes raw slices from the slice.

transport/GTK:
- Linux GSO super-buffer capped at the real UDP payload ceiling (65487)
  not 65535 — seg sizes ≥1024 could EMSGSIZE and latch GSO off
  process-wide, blamed on the network.
- GTK settings dialog no longer rewrites an unlisted-but-valid stored
  gamepad preference to "auto" on close.

Already fixed on main (verified stale, skipped): the reassembler FEC
ceiling, wants_decode_latency's third term, request_probe rollback +
the generalized probe watchdog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:26:43 +02:00
enricobuehler 76ac3cf867 fix(core): five sweep lows — seal-lane fallback, flush partial, codec echo, idle clamp, pair-name UTF-8
From the 2026-07-20 punktfunk-core quality sweep's adjudicated lows
(~/punktfunk-sweeps/punktfunk-core-2026-07-20.json), each with a
regression test:

- session: the two-lane seal's dead-worker fallback dropped the frame's
  back half and returned Ok with half an access unit; the corpse lane
  was also never respawned. The send-failure arm now reclaims the tail
  (single-lane seals the WHOLE frame) and both failure arms drop the
  lane so the next large frame respawns it. A recv-side death now
  surfaces as an error instead of silent truncation.
- reassemble: Reassembler::reset() left pending_partial parked, so a
  pre-flush stale partial survived Session::flush_backlog and was
  delivered as the first "frame" after a jump-to-live.
- caps: resolve_codec echoed a non-conformant multi-bit preferred byte
  verbatim (downstream from_wire folds it to HEVC — possibly outside
  the shared set). It now isolates one bit of the intersection.
- endpoint: stream_transport_idle only floored the value; an absurd
  operator-supplied idle timeout blew past QUIC's VarInt ms ceiling and
  panicked host startup through the expect. Clamped to 1s..1h.
- pairing: PairRequest::encode cut the device name at a raw byte-64
  boundary, splitting multi-byte UTF-8 (host showed U+FFFD forever);
  it now shares Hello's char-boundary truncate_to.

The frozen-FEC-ceiling finding (reassemble.rs:109) was already fixed on
main by the sweep's high-severity commit — skipped as stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:16:02 +02:00
enricobuehler f4ec18d528 chore(core): fix build.rs + crate-doc drift left by the W7/W8 splits
- build.rs: drop the stale `rerun-if-changed=src/client.rs` (the file
  became client/ in W7; the path never fires).
- lib.rs: the crate doc's module list named 6 of ~19 modules — add the
  missing ones (config/error/stats/input/reject/reanchor/render_scale/
  audio/wol + the quic-gated control-plane group), without intra-doc
  links to feature-gated modules.
- quic/mod.rs: the module doc still described the deleted `msgs`
  module; list the actual post-split module set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:08:39 +02:00
enricobuehler ce51a2ba74 refactor(core): decompose the client run_pump monolith into pump/ submodules
client/pump.rs was a single 1192-line async fn stacking six concerns.
It is now a 190-line orchestrator (same spawn order, same channel
wiring) over five focused modules:

- pump/handshake.rs   — connect + Hello/Welcome/Start + skew handshake +
                        hole punch + Session construction (HandshakeOut)
- pump/input_task.rs  — the gamepad snapshot/removal/arrival re-send
                        state machine + passthrough input forwarding
- pump/control_task.rs— the control-stream select loop (renegotiation,
                        probes, bitrate acks, clock re-sync, clip
                        metadata), bundled as ControlTask
- pump/datagram_task.rs — the datagram tag demux + rumble reorder gate
- pump/data.rs        — the blocking data-plane pump (loss reports, ABR
                        + capacity probe, jump-to-live detectors,
                        standing-latency bleed), bundled as DataPump

Every body moved verbatim (dedent + arg-struct destructures aliasing
the original binding names); no per-frame indirection added — the pump
loop, its locals, and the hot-path shape are byte-equivalent. The
Ctrl/DataPump arg structs exist only to stay under too_many_arguments.

196 lib tests pass; clippy clean on --features quic AND
--no-default-features (--all-targets); include/punktfunk_core.h
byte-identical; fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:07:14 +02:00
enricobuehler eacaaa5cd8 refactor(core): peel session.rs anti-replay, perf telemetry, and seal lane into submodules
session.rs (1203) sharpens down to the two hot-path state machines +
lifecycle (887): ReplayWindow + seq_of + their tests -> session/replay.rs;
the PUNKTFUNK_PERF trio PumpPerf/SealPerf/TimedCoder -> session/perf.rs;
the Phase-1.5 lane machinery SealLane/SealJob/seal_wire_slice/
TWO_LANE_MIN_PACKETS -> session/seal.rs. Facade pattern (session.rs stays
the parent file); pub use keeps session::{PumpPerf,SealPerf} stable and
lib.rs re-exports are untouched. Pure code motion + pub(super) bumps —
seal_frame_inner/poll_frame/poll_input bodies unchanged; the
wire-equivalence tests stay co-located with the seal path they pin.

196 lib tests pass, clippy --features quic --all-targets clean, fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:56:26 +02:00
enricobuehler e0a1edb9d4 refactor(core): co-locate the quic wire tests with their modules
quic/tests.rs (1813 lines, 43 tests) was the W7 split's leftover: the
source moved into handshake/caps/control/clock/pairing/pake/datagram/
endpoint/clipstream/io but every test stayed in one monolithic file.
Each test now lives in a #[cfg(test)] mod tests at the foot of the
module it exercises, verbatim. The two CompositorPref/GamepadPref
wire/name tests moved to config.rs (where those enums live), so they
now also run under --no-default-features. The clip_loopback and
ctrl_framing integration mods share connect_pair via a cfg(test)-only
quic/test_util.rs.

Test-only motion: 196 lib tests pass unchanged on macOS, clippy
--features quic --all-targets clean, include/punktfunk_core.h
byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:53:36 +02:00
enricobuehler a53369bf21 fix(ci): green main again — clock_sync callers, two clippy denials, an unused import
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m11s
apple / swift (push) Successful in 1m20s
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 10s
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-noble.Dockerfile, punktfunk-rust-ci-noble) (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 9s
ci / bench (push) Successful in 7m0s
flatpak / build-publish (push) Successful in 6m25s
deb / build-publish (push) Successful in 9m53s
docker / deploy-docs (push) Successful in 28s
release / apple (push) Successful in 9m25s
deb / build-publish-host (push) Successful in 10m0s
windows-host / package (push) Successful in 15m34s
apple / screenshots (push) Successful in 6m30s
arch / build-publish (push) Successful in 18m15s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m6s
android / android (push) Successful in 19m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m6s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m3s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m37s
ci / rust (push) Successful in 26m59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m31s
`ci.yml`'s clippy gate has been failing on main since the pre-0.16.0 sweep landed, and
because clippy stops at the first crate it can't compile, the visible error was only
ever the first of four. `ci.yml` is not tag-triggered, so v0.16.0 cut and shipped over
a red main; none of this reaches the release artifacts (the two E0308s are in a test
binary and a dev tool, and the rest are lints) but the gate has been blind since.

Fixed, in the order clippy surfaced them:

- clients/probe failed to COMPILE (E0308, x2). `810d918d` moved `clock_sync` onto the
  resumable `io::MsgReader` but only updated the client pump, leaving the probe passing
  a bare `&mut RecvStream`. The probe now wraps the control stream in a `MsgReader` at
  `open_bi` and threads that everywhere — Welcome, the --remode and --bitrate watchers,
  and the speed-test result read — which is also what the refactor was for: those reads
  sit behind timeouts and `select!`, exactly where a straddling frame would desync the
  stream for the rest of the run.

- pf-capture: `SPA_META_Cursor as u32` is a `u32 -> u32` no-op
  (`clippy::unnecessary_cast`). Line 1274 already passes the same constant uncast, so
  the type is not in question.

- pf-inject: `noop as usize` on the SIGUSR1 wake handler added in `986402f7` trips
  `clippy::function_casts_as_integer`; goes via `*const ()` as the lint asks. Same value
  in the `usize`-typed `sa_sigaction` slot.

- punktfunk-core: the `ctrl_framing` test module's `use super::*` is unused, which
  `-D warnings` promotes to an error.

Verified with CI's own commands on a Linux box (this is all Linux-gated code, so a Mac
cannot check it): `cargo clippy --workspace --all-targets --locked -- -D warnings`
finishes clean, `cargo build --workspace --locked` succeeds, and
`cargo test --workspace --locked` exits 0 with no failures — including punktfunk-core's
196-test suite, which covers the control-stream framing the probe change touches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:49:49 +02:00
enricobuehler dd558be55b fix(core/client): fix the four ways a speed-test probe corrupts ABR and the report tick
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m2s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 53s
deb / build-publish (push) Successful in 12m22s
ci / bench (push) Successful in 6m13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m32s
ci / rust (push) Failing after 9m2s
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 1m22s
release / apple (push) Successful in 9m50s
windows-host / package (push) Failing after 13m7s
deb / build-publish-host (push) Successful in 13m1s
arch / build-publish (push) Failing after 14m11s
apple / screenshots (push) Failing after 3m37s
android / android (push) Successful in 14m56s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6m8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m32s
flatpak / build-publish (push) Failing after 8m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m2s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m33s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m32s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m13s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m0s
docker / deploy-docs (push) Successful in 26s
There is one `ProbeState` per session and no correlation id, and two independent
requesters share it: the pump's startup link-capacity probe and the embedder's
`NativeClient::request_probe` (the shipped "Test connection" in the Windows GUI
and the Linux GTK app). The startup path had a busy check, a watchdog and a
window rebase; the embedder path had none of them, and the two collide by
default — both shipped speed tests call `request_probe` on the statement right
after `connect()`, and the pump's probe fires 2 s later, inside that burst.

Four defects, one cluster:

- The pump overwrote the shared slot unconditionally. Mid-burst it drops
  `base_packets`/`base_bytes`, the pump re-snapshots them against the host's
  full-burst denominator, and the user's speed test reports roughly an order of
  magnitude low; if the result lands first instead, the reset wipes `done` and
  the embedder's poll loop never sees its own measurement. Now it defers and
  retries rather than stealing the slot.

- An unanswered embedder probe never timed out. `probe_active` gates the entire
  report tick — LossReport, the ABR window feed, the standing-latency ladder and
  a pending ClockResync all live inside it. A host that ignores ProbeRequest is
  an anticipated configuration (the startup path was given a 6 s timeout for
  exactly that) so the embedder path could latch `active` forever and silently
  switch off every adaptation mechanism for the rest of the session. Now a
  watchdog covers a probe of either origin.

- `request_probe` latched `active` before `try_send`, so a full or closed ctrl
  channel returned `Closed` to the caller while leaving the session wedged in
  the state above. It now rolls back, mirroring the startup path.

- Only the startup path rebased the ABR window past the burst. Probe filler is
  counted into `bytes_received` for every accepted datagram but never reaches
  the decoder, and the tick is suppressed for the whole burst, so the first
  post-burst window read the burst rate as `actual_kbps`. That poisons
  `proven_kbps`, a monotone high-water mark that is never decayed, which
  disables the x1.5 evidence-gated climb guard for the session and authorizes a
  climb into a rate the decoder has never carried. The rebase now happens on the
  falling edge of any probe, and covers the loss/packet anchors too so the first
  post-burst LossReport isn't divided by a filler-inflated packet count.

Also: `wants_decode_latency` advertised on two of the three terms the pump
actually requires to arm ABR, omitting `resolved_bitrate_kbps > 0`, so against
an old host that reports no rate an embedder fed decode latency to a controller
that never runs.

No wire or ABI change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 07:42:37 +02:00
enricobuehler 810d918d36 fix(core/quic): make the control-stream read cancel-safe
`io::read_msg` frames a message with two `quinn::RecvStream::read_exact` calls,
and quinn documents `read_exact` as explicitly NOT cancel-safe: the bytes it has
already taken out of the stream live only in the future's own buffer and nothing
puts them back on drop. Both long-lived control loops drive that read from a
`tokio::select!` arm — the client pump alongside `ctrl_rx.recv()` and the resync
tick, the host alongside probe/reconfig/clip-offer channels — and neither uses
`biased;`, so any sibling that becomes ready ends the iteration and drops a
partially-progressed read. `clock_sync` has the same shape via
`tokio::time::timeout`, which can fire mid-frame before the session even starts.

A control frame only has to straddle two wakeups for this to bite: a ClipOffer
carries up to 16 kinds x 128 bytes of MIME, ~2 KB, which exceeds one QUIC packet
and is subject to the pacer; any frame whose second half is lost or reordered
does it too. Losing the consumed length prefix misaligns the stream permanently
— the next read takes two payload bytes as a length, so Reconfigured,
ProbeResult, BitrateChanged, ClockEcho and ClipState all decode as garbage and
are silently dropped, and a bogus length up to 64 KiB parks the read forever.
Mode switches, adaptive bitrate, mid-stream clock resync and clipboard are dead
for the rest of the session; only a reconnect recovers, and the log shows at
most one `warn!`.

Add `io::MsgReader`, which keeps the frame in progress in the reader rather than
the future and reads via quinn's cancel-safe `read`, and switch the three
cancelling sites to it (client control loop, host control loop, clock_sync).
The sequential handshake/pairing callers keep the plain `read_msg`, whose doc
comment now states the constraint. No wire bytes and no ABI change — only how
the same length-prefixed frames are assembled.

Tests: a frame split across two wakeups with the read cancelled in between must
resume and leave the following frame correctly framed (confirmed to fail — it
hangs on the desynced stream — against the old behavior), plus a zero-length
frame round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 07:42:37 +02:00
enricobuehler 7b2cdf5a7a fix(core/net): bind the data plane to the authenticated peer + stop adaptive FEC wedging large frames
Four defects from the punktfunk-core quality sweep, all in the data plane.

transport/udp: the hole-punch adopted the source address of ANY datagram whose
first 8 bytes matched PUNCH_MAGIC — a fixed public constant with no key, nonce
or session id — and the authenticated QUIC peer was passed only as the
no-punch fallback, so it was never used to validate. Hole-punch is the default
bring-up path (it is skipped only for a fixed --data-port), and the data socket
is an OS ephemeral, so spraying the ephemeral range during the 2500 ms punch
wait let anyone steal the video plane: the legitimate client is then filtered
out by the connect() and gets nothing, while QUIC stays healthy so no reconnect
fires. With a spoofed source the same 8 bytes aim a full-rate stream at a third
party. Take the authenticated peer IP and require the punch to match it — only
the PORT is in question (that is what a NAT remaps and what the punch exists to
discover); the client dials the same host IP as its QUIC connection, so a NAT
presents one source IP for both planes. Also budget each read from the REMAINING
window, so off-peer datagrams cannot stretch the wait past punch_timeout.

transport/udp: the punch keepalive treated every send error as fatal and broke
out of its loop permanently and silently. It holds a clone of the connected,
non-blocking data socket — exactly the socket whose transient conditions this
module defines and documents in is_transient_io. One ENOBUFS from a full wlan tx
queue or an ENETUNREACH during an AP roam killed the only thing holding the NAT
mapping open; the path recovers, video keeps flowing, and the stream dies later
when the idle timer expires the mapping during a static scene. Route it through
is_transient_io like every other send site in the file.

packet: adaptive FEC moved fec_percent live (host bands it 1..=50 while Welcome
advertises 10) but the receiver's per-block acceptance ceiling was computed once
from the negotiated percentage and never re-derived. Once FEC ramped, every
packet of a maximal block failed `total > max_total_shards`, the block never
accumulated a shard, the frame aged out, and the resulting loss drove FEC higher
still — large frames wedged at 100% loss exactly when FEC was meant to rescue
the link. Fixed on both sides, because hosts and clients update independently:
the sender clamps per-block parity to the ceiling the peer negotiated, and the
receiver sizes that ceiling from the whole clamp range rather than a stale
snapshot of it.

No wire bytes and no C ABI signature change; WIRE_VERSION and ABI_VERSION are
unchanged. Regression tests cover all three (the punch tests were confirmed to
fail without the fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 07:42:37 +02:00
enricobuehler a784682d4c fix(client/net): split the receipt stamp from the pull + bleed standing latency
ci / docs-site (push) Successful in 56s
apple / swift (push) Successful in 1m14s
ci / web (push) Successful in 2m30s
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 7s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (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 9s
ci / bench (push) Successful in 5m57s
release / apple (push) Successful in 9m8s
deb / build-publish (push) Successful in 9m33s
arch / build-publish (push) Successful in 13m14s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m1s
deb / build-publish-host (push) Successful in 10m24s
android / android (push) Successful in 14m56s
apple / screenshots (push) Successful in 6m43s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m29s
windows-host / package (push) Successful in 15m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 5m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m51s
The two-pair investigation (wired Mac clients stuck at a rock-steady
~18-19 ms "network" that survived the load ending and cleared only on
reconnect) exposed two structural gaps, one of measurement and one of
recovery:

- Receipt was stamped at the hand-off PULL (Swift nextAU, pf-client-core,
  Android decode loops), not at reassembly completion — so any client-side
  standing state between the reassembler and the pull read as NETWORK
  latency, undiagnosable from the HUD. ABI v9: `PunktfunkFrame`/`Frame`
  grow `received_ns`, stamped by `Session::poll_frame` as the AU crosses
  the session boundary. Every embedder now uses the core stamp; the Apple
  client keeps the pull instant as `AccessUnit.pulledNs` and shows the
  receipt→pull wait as its own "client queue" term (detailed HUD tier from
  2 ms + a `queue_p50` stats-log field). Decode stages keep their pull
  anchor on all platforms, so no historical stage shifts meaning.

- The jump-to-live detectors deliberately ignore anything under 6 queued
  frames / 400 ms behind — so a small, constant, loss-free elevation (a
  sub-frame standing backlog, or a stale clock offset after a wall-clock
  step/slew) is carried for the rest of the session. New third detector
  (`StandingLatency`, unit-tested ladder): window-MIN one-way delay
  ≥ 10 ms above the session floor with zero loss for ~4.5 s escalates
  gently — a free clock re-sync first (an applied re-sync re-bases the
  floor), then at most 3 flush+keyframe bleeds sharing the jump-to-live
  cooldown, then a loud disarm naming what it means. Loss windows reset
  the run: congestion belongs to FEC/ABR, not this detector.

Also: mid-stream re-sync apply/discard logs debug→info — they are the
forensic trail for the stale-offset case and were invisible in the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 23:38:11 +02:00
enricobuehler 08a397cf08 fix(transport): treat Windows WSAENOBUFS as a transient send drop, not a stream teardown
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m18s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m14s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m17s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 57s
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 21s
apple / swift (push) Successful in 1m20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 7m7s
flatpak / build-publish (push) Failing after 8m7s
docker / deploy-docs (push) Successful in 22s
release / apple (push) Successful in 9m3s
windows-host / package (push) Failing after 11m41s
deb / build-publish (push) Successful in 13m34s
ci / rust (push) Failing after 14m2s
arch / build-publish (push) Successful in 14m59s
android / android (push) Successful in 16m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m16s
apple / screenshots (push) Successful in 6m38s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 7m16s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m38s
`is_transient_io` only recognized `ENOBUFS` on unix; on Windows the
`#[cfg(not(unix))]` arm returned `false`, so `WSAENOBUFS` (10055) — which
Rust maps to `ErrorKind::Uncategorized`, missing the `WouldBlock` arm —
propagated out of the USO `send_gso` path through `Session::send_sealed`
and killed the session with `native::stream` "send failed — stopping
stream". A high-bitrate keyframe burst (one `WSASendMsg` USO super-buffer
is up to ~512 segments) momentarily exhausts the socket send buffer / AFD
non-paged pool; it's a lossy drop that FEC + the next frame recover,
exactly like the unix `ath11k` `ENOBUFS` case the classifier already
handles. Fires independently of client platform (Windows/macOS clients
both saw the crash) because it's the Windows *host's* send socket.

Add a `#[cfg(windows)]` arm matching `WSAENOBUFS` plus the
`WSAENET*`/`WSAEHOST*` network-path family (the Windows counterparts of
the droppable unix set), and extend the classifier test with per-platform
raw-errno coverage so the Windows CI runner exercises the 10055 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 01:43:15 +02:00
enricobuehler bbe4380b41 perf(latency): T1.1 frame-driven encode trigger + T1.4 time-based flush thresholds
ci / web (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m14s
apple / swift (push) Successful in 1m24s
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 22s
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 25s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
docker / deploy-docs (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-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 / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
deb / build-publish (push) Failing after 4m39s
arch / build-publish (push) Failing after 4m46s
ci / rust (push) Failing after 4m53s
ci / bench (push) Successful in 6m8s
android / android (push) Successful in 13m40s
apple / screenshots (push) Successful in 6m26s
design/latency-reduction-2026-07.md tier 1, remaining halves:

- T1.1: the native encode loop wakes on the capture's ACTUAL arrival instead of
  sampling at a free-running tick — deletes the sample-and-hold (~half a frame
  interval on average, a full one worst-case: ~8ms avg @60fps). New
  Capturer::supports_arrival_wait/wait_arrival pair (IDD-push waits its
  frame-ready event against the shared-header token; the PipeWire portal blocks
  its channel with a pending stash); backends without an arrival signal — and
  PUNKTFUNK_FRAME_DRIVEN=0 — keep the legacy tick bit-identically. A
  0.9x-interval rate floor caps encode at ~1.11x target when the compositor
  outruns the session; a +0.5x-interval keepalive keeps static desktops
  re-encoding at 1.5x-interval cadence. Pacing deadlines re-anchor to the
  actual submit so they can't drift against the arrival clock. GameStream
  plane untouched.
- T1.4: the jump-to-live detectors run on WALL-CLOCK now (STANDING_TIME /
  FLUSH_AFTER = 250ms) instead of 30-frame counts whose meaning scaled with
  fps (500ms @60 but 125ms @240 — and stretching further under T1.1's slower
  static-scene repeats). The queue trip also requires depth still >= high, so
  a hysteresis-band hover can't fire on elapsed time alone.

Validated: .21 Linux 185 core + 177 host + pf-capture tests, clippy
-D warnings; .133 Windows cargo check of pf-capture + punktfunk-host green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:31:49 +02:00
enricobuehler aedee2a4e3 perf(latency): tier-0 attribution + tier-1 send-path levers from the latency plan
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 54s
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 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 8s
apple / swift (push) Successful in 1m23s
ci / rust (push) Failing after 4m15s
arch / build-publish (push) Failing after 4m35s
deb / build-publish (push) Failing after 4m4s
docker / deploy-docs (push) Successful in 24s
ci / bench (push) Successful in 5m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m45s
windows-host / package (push) Failing after 9m8s
release / apple (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m25s
flatpak / build-publish (push) Failing after 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 14m9s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 9m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 12m26s
design/latency-reduction-2026-07.md T0.1/T0.2/T1.2/T1.3:

- T1.2 rate-capped front-loaded pacing: the paced overflow's budget is now
  min(0.9x slack, overflow wire time at ~3x the live encoder bitrate)
  (PUNKTFUNK_PACE_FACTOR, 0 = legacy deadline-only spread). A 300 KB-1 MB
  frame's tail leaves in ~2-5 ms instead of smearing across ~15 ms at 60 fps;
  GameStream schedule byte-identical (pins unchanged).
- T1.3 data-first wire order: packetize emits every block's data shards before
  any parity (per-block parity pools keep all blocks' parity alive for the
  second pass), so lossless completion stops waiting behind the parity tail.
  EOF = last emitted packet; receiver already order-agnostic.
- T0.1 staged 0xCF: HostTiming gains an append-extensible per-stage tail
  (queue/encode/pace us; seal+channel-wait derived as residual) - no cap bit
  needed, old peers read the 13-byte prefix. Joined client-side into
  Stats::host_{queue,encode,xfer,pace}_ms, the OSD detailed tier, and the
  probe's report.
- T0.2 true on-glass present timing: VK_KHR_present_id/present_wait enabled
  when supported; a PresentTimer waiter thread resolves each present id to
  real visibility, replacing the submit-time display stamp (which undercounts
  by up to a refresh and hides a silent-FIFO standing queue).

Validated on .21: core 185 + host 185 tests, pf-presenter 19, clippy
-D warnings across all five touched crates; loss-harness recovery curve
unchanged; C ABI harness round-trips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:13:08 +02:00
enricobuehler 871ebb31ce feat(clients): render-scale setting on every client — shared punktfunk_core::render_scale
Client-side supersampling/downscaling: the client asks the host to render
and encode at chosen-resolution × scale (the host does no scaling) and the
presenter rescales the decoded frame to the display. >1 supersamples for
sharpness; <1 lightens the host GPU and the link. Default 1.0 = Native, the
prior behavior.

The geometry lives once in punktfunk_core::render_scale (multiply, preserve
aspect ratio, floor to even, clamp to the codec's per-axis ceiling — 4096
for H.264, 8192 otherwise), the Rust twin of the Apple client's
RenderScale.swift, consumed by the native session client, the presenter's
match-window path, the Windows/Linux settings UIs, Decky, and Android
(settings + host connect + unit test).

Implemented and platform-verified by the Apple-client-features session
(Linux+Android+Apple green there); the punktfunk-core wiring
(pub mod render_scale) is restored here after being lost in a working-tree
reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:15 +02:00
enricobuehler 2064c0780c merge(core): reconcile the W7/W8 client refactor with origin's shared-clipboard feature
origin/main landed the shared clipboard (design/clipboard-and-file-transfer.md) while
this branch split quic/msgs.rs -> quic/{caps,control,...} and client.rs ->
client/{mod,control,worker,pump,planes,...} (W7) and deleted the two monoliths. The
feature had modified both deleted files, so its delta is re-applied onto the split
instead of resurrecting the monoliths:

  - HOST_CAP_CLIPBOARD                         -> quic/caps.rs
  - MSG_CLIP_* / CLIP_* consts, the six Clip*
    structs, and their encode/decode impls     -> quic/control.rs (beside the clock codecs)
  - CtrlRequest::{ClipControl,ClipOffer} +
    Negotiated.host_caps                        -> client/control.rs
  - WorkerArgs.{clip_event_tx,clip_cmd_rx}      -> client/worker.rs
  - CLIP_EVENT_QUEUE                            -> client/planes.rs
  - NativeClient clip fields, the 7 clip_* /
    host_caps / next_clip methods, connect()
    channel wiring                              -> client/mod.rs
  - the control-task encode/decode arms and
    the clipboard-task spawn                     -> client/pump.rs

Cargo.lock reconciled (adds pf-clipboard), punktfunk-host/Cargo.toml unions the W6
pf-* subsystem deps with pf-clipboard, and include/punktfunk_core.h is the cbindgen
union (clipboard + rumble C-ABI). punktfunk-core builds --all-features and its 174
lib tests pass, including quic::tests::clip_loopback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:38:11 +02:00
enricobuehler 13b1f36d4a feat(core,clients): one rumble policy engine for every platform (rumble root fix D)
punktfunk-core client/rumble.rs: a per-connection policy engine consumes seq-gated wire
updates and emits EFFECTIVE actuator commands — re-emits on renewals (duration APIs stay
re-armed), self-silences at the v2 lease, a UNIFORM 1 s legacy-host staleness replacing the
per-platform zoo (Apple 1.6 s / Android 60 s / SDL 1.5 s / Deck 1 s), quirk-declared
actuator keepalives (Deck 40 ms + LSB dedupe-defeat jitter), and one stop per buzzing pad
on connection close. Per-pad mailbox semantics: a stalled embedder wakes to ONE current
command, and a stop can structurally never be the update an overflowing queue drops.

New API/ABI: NativeClient::{next_rumble_command,set_rumble_quirks} +
punktfunk_connection_next_rumble_cmd/_set_rumble_quirks (next_rumble/next_rumble2 stay for
un-migrated embedders; both consumers are fed). Migrations DELETE the platform forks:
pf-client-core loses RumbleState + the Deck keepalive loop + LEGACY_RUMBLE_CEILING_MS and
physically silences a slot at close; Android loses the 60 s legacy one-shot (backstop
repack, cancel-on-zero); Apple loses envelopeDeadline + sessionStaleSeconds + both tick
watchdogs (CoreHaptics realization untouched; mac xcframework rebuilt locally).

design/rumble-root-fix.md par. D. Engine 10/10 unit tests; core tests 176 Linux / 175
Windows + clippy -D warnings; swift build + RumbleTuningTests; Kotlin + android-native
compile green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:07:32 +02:00
enricobuehler ffa63a74f2 refactor(core/W7): split client.rs into client/ facade + submodules
Turn the 2674-line client.rs into a client/ directory module (mod.rs facade +
8 submodules) behind glob/`use self::` re-exports, so crate::client::X paths
(NativeClient, ProbeOutcome, AudioPacket, display_hdr_env_override) stay
byte-stable. Leaf lifts: frame_channel.rs (the FIFO hand-off + jump-to-live
consts + DecodeLatAcc), recovery.rs (RfiRecovery loss-range detector),
probe.rs (ProbeState/ProbeOutcome), planes.rs (side-plane queues + AudioPacket),
control.rs (CtrlRequest/Negotiated), worker.rs (WorkerArgs + reject_from_close),
pairing.rs (NativeClient::pair). The per-frame pump moves WHOLE as a plain
`pub(super) async fn run_pump` (was worker_main) — the only edit is the
signature line: no trait object, no Box, no per-frame allocation or indirection.
NativeClient + its public impl + Drop + the cfg-gated thread-pin/hot-tid helpers
stay in the facade. Visibility bumps are pub(crate) (struct + each field for
WorkerArgs/Negotiated/ProbeState; FrameChannel + each method); reject_from_close
is pub(crate) (sibling access). No behavior change.

Verified: Linux clippy (quic + no-default, -D warnings) + full cargo test;
Windows clippy (both) + test --lib; macOS clippy (apple thread-pin variant) +
165 lib tests. On-glass jump-to-live + ABR smoke still owed (pump is a pure
relocation, so this is a formality) per the plan's pump gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:29:16 +02:00
enricobuehler 716875dd09 refactor(core/W7): split quic/msgs.rs into handshake/caps/control/pairing
Break the 1302-line quic/msgs.rs into four flat sibling modules behind the
quic facade's glob re-exports, so every crate::quic::X path stays byte-stable:
handshake.rs (Hello/Welcome/Start + codecs), caps.rs (video-cap bits, codec &
chroma negotiation, ColorInfo), control.rs (typed CTL_MAGIC messages + frame),
pairing.rs (SPAKE2 ceremony messages). msgs.rs is deleted; quic/mod.rs gains the
four `mod`/`pub use` lines and the `pub use crate::reject::*` hoist (moved up from
msgs.rs). Pure move; no wire-format or behavior change. Private helpers
(truncate_to, put_bytes, get_bytes) stay with their sole callers; no visibility
changes.

Verified both platforms from clean HEAD snapshots: Linux clippy (quic +
no-default, -D warnings) + full cargo test (157 lib + integration); Windows
clippy (both) + test --lib (156).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:08:45 +02:00
enricobuehler ef736cb9d7 refactor(core/W7): split transport/udp.rs into udp/ facade + per-OS backends
Turn transport/udp.rs into a udp/ directory module: the cross-platform core
(UdpTransport, is_transient_io, spawn_data_punch, the Transport trait impl) stays
in mod.rs; the platform batched-I/O backends move to udp/{linux,windows,apple}.rs.
The trait impl is kept whole -- its per-OS send_batch/send_gso/recv_batch methods
become cfg-gated one-line delegators to pub(super) free fns that take &UdpTransport
(byte-identical bodies, self -> t). transport/mod.rs is unchanged (re-exports still
resolve; udp/mod.rs re-exports windows::send_uso_all). No behavior change.

Module gates: linux = any(linux, android) (Android uses sendmmsg/recvmmsg via its
bionic binding); windows = windows (USO); apple = all(unix, not(any(linux,android)))
(recvmsg_x on Darwin, recv-loop on BSD). GSO stays linux-only.

Verified on all four target families from clean HEAD snapshots: Linux clippy
(quic + no-default, -D warnings) + full test; Windows clippy (both) + test --lib
(156); macOS check (apple recvmsg_x path); aarch64-linux-android check (android_mmsg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:01:53 +02:00
enricobuehler 4ef90d586d feat(clipboard): wire protocol + client-core task for shared clipboard (Phase 0)
The portable shared-clipboard plane in punktfunk-core, all behind the `quic`
feature (design/clipboard-and-file-transfer.md §3):

- Control messages 0x40–0x44 (ClipControl / ClipOffer / ClipFetch...) and the
  HOST_CAP_CLIPBOARD capability bit, negotiated in the Welcome caps.
- Per-transfer QUIC bi-streams ("PKFs" magic) for lazy fetch of offered content,
  with ClipFetchHdr status/size framing (quic::clipstream).
- The §3.5 portable wire-MIME vocabulary (text/plain;utf-8, text/html, text/rtf,
  image/png) shared by both ends.
- Client-side clipboard task (client.rs) + C ABI surface bumped to v8 (abi.rs,
  regenerated include/punktfunk_core.h).
- Loopback transport tests (quic::tests).

No OS clipboard integration yet — that is the host backends (Phase 1/3) and the
macOS client (Phase 1).

Ported from feat/shared-clipboard (af3a7d8c, pre-W6 base) onto current main;
three deliberate deviations from the original commit:
- ABI v6 → v8: main took v6 (reanchor gate) and v7 (typed connect rejection)
  in the meantime; the clipboard C surface re-lands as v8.
- CLIP_CANCELLED_CODE 0x60 → 0x70: main's pairing-rejection close codes claimed
  the 0x60–0x67 block; the vocabularies stay disjoint on purpose.
- Negotiated.host_caps coexists with main's 6-tuple host_caps plumbing: main
  needs the worker-local copy for gamepad snapshots, the clipboard path needs it
  across ready_tx to build the NativeClient handle (punktfunk_connection_host_caps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:50:37 +02:00
enricobuehler 93c8dc4712 refactor(core/W7): split packet.rs into packet/ facade + submodules
Turn the 1446-line packet.rs into a packet/ directory module (mod.rs facade
+ header/packetize/reassemble/tests) behind glob re-exports, so every
crate::packet::X path stays byte-stable. Pure move: the header consts +
PacketHeader -> header.rs; Packetizer -> packetize.rs; the Reassembler cluster
(kept WHOLE -- disjoint-borrow hot path) + loss-window consts -> reassemble.rs;
the inline #[cfg(test)] block -> tests.rs. Sole visibility change:
LOSS_WINDOW_NS -> pub(super) (a test imports it). No behavior change.

Verified on both platforms from a clean HEAD snapshot: Linux clippy
(--features quic and --no-default-features, --all-targets -D warnings) + full
cargo test; Windows clippy (both feature sets) + cargo test --lib (156 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:48:48 +02:00
enricobuehler 6824c1cc0c feat(core): Automatic bitrate climbs only through validated territory — utilization gate + proven-throughput cap
The controller's target is only a promise: on calm content the encoder
emits a fraction of it, every window looks clean while proving nothing,
and the climb drifts the target into rates the decoder has never seen.
The first motion spike is then the first real test — it fails, and the
decoder is overloaded for the two-window backoff latency (the reported
settle-calm-then-spike stutter).

Three changes, all client-side (no wire/ABI impact, old hosts unaffected):

- Climb gate: a clean window authorizes a climb only when its ACTUAL
  delivered throughput reached 3/4 of the current target — the target was
  genuinely tested. Calm windows still bank clean credit; the first
  loaded window after a clean run climbs immediately.
- Proven-throughput cap: climbs step at most x1.5 past the session's
  high-water mark of delivered-and-digested (decode-latency-flat)
  throughput, so slow start becomes a bounded experiment instead of a
  blind doubling. High-water never decays: calm periods keep a validated
  target, so returning motion gets the full rate instantly; shrinking
  capacity (thermals) stays the reactive decode signal's job.
- Severe decode excursion: a >45 ms-over-baseline decode spike backs off
  after ONE window instead of two — the overload is already on screen.

The pump feeds the window's wire-byte throughput; the byte baseline is
rebased when the startup capacity probe completes so FLAG_PROBE filler
can't poison the proven mark with the link rate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:55:09 +02:00
enricobuehler 47587827ec refactor(host/W6.0): hoist GamepadEvent/GamepadFrame to punktfunk_core::input
apple / swift (push) Successful in 1m22s
release / apple (push) Successful in 6m6s
apple / screenshots (push) Successful in 4m58s
windows-host / package (push) Successful in 15m58s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m59s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m58s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m35s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m12s
decky / build-publish (push) Successful in 20s
android / android (push) Successful in 12m11s
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 10s
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 8s
ci / bench (push) Successful in 6m17s
arch / build-publish (push) Successful in 13m21s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Successful in 6m37s
deb / build-publish (push) Successful in 12m28s
ci / rust (push) Successful in 21m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m43s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m46s
First de-coupling for the host crate carve (plan §W6.0 / §2.4): the GameStream
(Moonlight-plane) decoded controller types were defined in gamestream/gamepad.rs — the
"junk drawer" — yet consumed 18× by the platform-neutral input injectors AND by the
Moonlight decode path. Once inject becomes pf-inject, reaching them via crate::gamestream
would be an illegal upward edge. Move the two types to core::input (below both planes;
inject already depends on core) and repoint every consumer. Also consolidate the
duplicated MAX_PADS onto the existing core::input::MAX_PADS. The gamestream BTN_* const
aliases stay for now (separate follow-up); decode()/rumble/tests remain in the Moonlight
plane, now importing the types from core.

Verified: Linux (home-worker-5) clippy -p punktfunk-core -p punktfunk-host --all-targets
-D warnings + gamepad tests green; Windows (192.168.1.158) clippy -p punktfunk-host
--features nvenc,amf-qsv --all-targets green (the inject/windows/* consumers compile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:49:38 +02:00
enricobuehler ecfa71212d chore: consolidate all in-progress parallel-session WIP
audit / bun-audit (push) Successful in 27s
windows-drivers / probe-and-proto (push) Successful in 49s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 34s
windows-drivers / driver-build (push) Successful in 1m37s
audit / cargo-audit (push) Successful in 3m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
ci / bench (push) Successful in 5m56s
windows-host / package (push) Failing after 5m17s
apple / screenshots (push) Successful in 4m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
arch / build-publish (push) Successful in 16m50s
android / android (push) Successful in 17m7s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 17m6s
ci / rust (push) Failing after 19m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m3s
flatpak / build-publish (push) Successful in 8m0s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m14s
release / apple (push) Successful in 5m44s
Wholesale commit of every uncommitted change across the tree, at the user's
explicit request — host refactor-campaign W1 (native.rs facade + native/ dir,
library/ + mgmt/ splits), Android, core. These streams were mid-flight and not
individually built/tested together; this supersedes the per-session HOLD
markers. Consolidating so everything lands on main in one pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:08:29 +02:00
enricobuehler 571e22bc0f refactor(core): consolidate the fingerprint-pinning verifier into core::tls
Per plan §2.5: the security-critical rustls fingerprint-pinning ServerCertVerifier
was hand-rolled three times — quic/endpoint.rs (PinVerify), pf-client-core
library.rs, punktfunk-tray status.rs — drifting copies on a trust boundary. Add
one canonical punktfunk_core::tls::PinVerify (+ cert_fingerprint) behind a light
`tls` feature (rustls + sha2 only, no QUIC runtime); `quic` now depends on it, and
quic::endpoint re-exports cert_fingerprint so that path stays byte-stable
(gamestream + pf-client-core reach it there).

- core::tls::PinVerify: new(pin) for the HTTP clients, with_observed(pin, slot)
  for the QUIC TOFU connect. Behavior-identical to all three originals (pin-check
  + real CertificateVerify signature verification; only hashes the leaf when a pin
  or observed slot needs it). Two focused unit tests anchor the boundary.
- quic/endpoint.rs: drop the private PinVerify, wire client_pinned through
  tls::PinVerify::with_observed.
- pf-client-core library.rs + tray status.rs: use the shared verifier; tray also
  routes load_pin through core cert_fingerprint and drops its direct sha2 dep,
  gaining only the light core `tls` feature (still no host dep, no QUIC runtime).

Verified on Linux (home-worker-5): clippy 0/0 for core(quic), core(tls),
pf-client-core, tray, host(nvenc,vulkan-encode,pyrowave); core 153 lib tests +
loopback 7/7 (pinned handshake) + c_abi round-trip green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:36:29 +02:00
enricobuehler 1f519d44f9 docs(core): backfill //! module docs on quic/{io,endpoint,pake}
The last three //!-less modules in the tree (plan §2.5 / §3.2):
  - io:       length-prefixed control-message framing (read_msg/write_msg)
  - endpoint: QUIC endpoint construction + transport tuning + the TOFU
              cert-pinning verifier (PinVerify)
  - pake:     SPAKE2 pairing key exchange

Docs only — no code, type, or wire-format change (cbindgen header byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:54:46 +02:00
enricobuehler 11045a0f70 chore: consolidate parallel-session WIP (HOLD — do not push)
Local snapshot of intermingled in-flight work, committed to unblock the encode
refactor (a clean ffmpeg_win.rs for the vbv-dedup follow-on). These hunks span
the same files and can't be cleanly split here; the commit bundles three
distinct workstreams that each belong in their own PR:

  - logging rework (~43 files: level re-tiering, structured fields, `?e`,
    hot-path flood latches)
  - conflicting-host detection (detect.rs + detect/{linux,windows}.rs + wiring
    in main.rs/mgmt.rs/Cargo.toml/docs/packaging)
  - standby-sink DWM-stall attribution (windows/display_events.rs + capture/
    vdisplay wiring)

NOT verified as a combination. NOT to be pushed until the refactor is done and
these are re-verified and reorganized into their proper per-workstream PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:42:53 +02:00
enricobuehler 01266ff18d feat(gamepad): SC2 Puck-dongle passthrough with the native 28DE:1304 topology
Community-contributed round 5 of the Steam Controller 2 passthrough,
reviewed + verified. A Puck-captured pad now presents the dongle's real
seven-interface identity (CDC pair, four controller HID slots, management
HID) instead of relabelling its reports as a wired 1302 — Steam's Puck
feature dances (wireless_transport / esb/bond / 0xB4 slot status) get
capture-shaped answers, and the wired identity's canned replies are
corrected to the real captures (attribute count, string-attr framing,
0xF2 firmware info, bcdDevice nibble encoding).

- new wire pref 10 = SteamController2Puck (Hello/Welcome byte; older
  peers degrade to Auto), selected by the Android capture link when the
  transport is a dongle, or by VID/PID in the degraded InputDevice path
- TRITON_RDESC is now the captured numbered descriptor (mouse/keyboard
  lizard collections + per-id vendor reports); unnumbered framing made
  hidraw mangle feature report 2 and Steam eventually closed the device
- interrupt-IN now queues sparse reports (battery/RSSI/wireless edges)
  instead of keeping latest-only, so a 250 Hz state packet can no longer
  erase them before the USB/IP poll observes them; EP0 SET_REPORT is
  split by wValue report type (OUTPUT parsed for rumble vs FEATURE)
- vendored usbip-sim: config attributes/max-power, IAD prefix + BOS
  descriptor support, correct BCD minor.patch encoding (Deck's 0x0300/
  0x0200 values are nibble-zero, so its bytes are unchanged), and
  full-speed interrupt pacing in ms (was 8 kHz from the HS formula)
- Triton feedback is serviced at 1 kHz while an SC2 backend exists so
  Steam's trackpad haptic writes reach the client unbatched

Verified: clippy -D warnings + 319 host tests green on Linux, core wire
tests green, Android kit/app compile + unit tests green. On-glass Puck
retest owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:47:52 +02:00
enricobuehler ff38933312 feat(core,apple,session): report decode latency from the Apple + Windows/Linux clients too
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m4s
ci / rust (push) Failing after 6m46s
decky / build-publish (push) Successful in 20s
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
ci / bench (push) Successful in 5m19s
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
arch / build-publish (push) Successful in 11m12s
windows-host / package (push) Successful in 14m52s
flatpak / build-publish (push) Successful in 7m10s
android / android (push) Successful in 16m0s
docker / deploy-docs (push) Failing after 17s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m8s
deb / build-publish (push) Successful in 14m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m18s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m31s
apple / swift (push) Successful in 5m19s
release / apple (push) Successful in 27m46s
apple / screenshots (push) Successful in 19m39s
Extends 56f9c8c4 (the Automatic-bitrate decode signal, core + Android) to the remaining
clients, so every platform caps Automatic at its real decoder limit instead of the network
link ceiling — the fix for a fast LAN feeding a slower hardware decoder.

- core/abi: punktfunk_connection_report_decode_us + _wants_decode_latency expose the
  NativeClient methods to the C-ABI embedders (regenerated punktfunk_core.h, additive only).
- apple: PunktfunkConnection wrappers + Stage2Pipeline reports received→decoded from the
  VideoToolbox decode-completion callback — every decoded frame, before the newest-wins ring
  can drop the backlog. Stage-1 (AVSampleBufferDisplayLayer, no per-frame decode callback)
  stays network-only; stage-2 is the metered path.
- windows/linux: the shared punktfunk-session client (pf-client-core) links core directly, so
  it calls the NativeClient methods — report received→decoded from the pump, gated on
  wants_decode_latency. Exact for the synchronous D3D11VA/software decode; received→submit
  (still the decoder-input backpressure signal) for the async Vulkan-Video path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:43:54 +02:00
enricobuehler 56f9c8c4b4 feat(core,android): Automatic bitrate caps at the client decode limit, not the link ceiling
windows-host / package (push) Successful in 2m7s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 34s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m31s
ci / rust (push) Failing after 6m39s
decky / build-publish (push) Successful in 30s
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 8s
ci / bench (push) Successful in 5m17s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m0s
arch / build-publish (push) Successful in 16m33s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
apple / swift (push) Successful in 5m13s
android / android (push) Successful in 17m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10m5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9m0s
deb / build-publish (push) Successful in 12m2s
flatpak / build-publish (push) Failing after 8m6s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m22s
release / apple (push) Successful in 27m42s
apple / screenshots (push) Successful in 18m40s
The Automatic bitrate controller only reacted to network signals (loss, capture→received
OWD, FEC-unrecoverable frames, jump-to-live flush), so on a fast LAN feeding a slower
mobile HW decoder it slow-started straight to the link-probe ceiling and parked there —
backlogging frames inside the decoder, where those signals never register, and choking it.
Reported on a Snapdragon 8 Gen 1: Automatic pinned ~500 Mbps with unusable latency.

Feed the client's decode-stage latency (received→decoded) into the controller as a
first-class signal, symmetric with the existing OWD one: a rise over its rolling-min
baseline ends the slow-start climb and, sustained over two windows, backs the rate ×0.7
down to the real decode limit — so Automatic settles where the decoder keeps up.

- core/abr: on_window gains decode_mean_us; a decode_means rolling-min baseline +
  DECODE_RISE_US (15 ms) fold a decode rise into the bad-window logic.
- core/client: per-frame report_decode_us accumulator, drained to a window mean by the
  data-plane pump; wants_decode_latency() gate (Automatic, non-PyroWave) lets embedders
  skip the measurement where it's ignored. Re-target log prints the driving signals.
- android/decode: report the decode stage on both the sync and async decode paths,
  HUD-independent, measured from the AU leaving next_frame (so codec-input backpressure
  is included) and excluding the vsync present wait.

Apple/Windows report_decode_us calls to follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:28:11 +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 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 1fc9ef0050 feat(core,host,clients): typed pairing rejections — every client says WHY, not "not accepted"
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 52s
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 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 44s
ci / bench (push) Successful in 5m56s
flatpak / build-publish (push) Successful in 6m41s
docker / deploy-docs (push) Successful in 27s
apple / swift (push) Successful in 4m40s
deb / build-publish (push) Successful in 12m7s
arch / build-publish (push) Successful in 14m10s
android / android (push) Successful in 17m16s
ci / rust (push) Successful in 17m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m50s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m15s
windows-host / package (push) Successful in 14m33s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m55s
release / apple (push) Successful in 25m31s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m49s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m11s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m9s
apple / screenshots (push) Successful in 19m41s
A host's pairing-gate rejections (not armed / bound to another device /
rate-limited / identity required / denied / approval timeout / superseded /
wire-version mismatch) used to drop the connection with a bare code-0 close,
and every client collapsed that — plus plain unreachability — into one
"wrong PIN / not accepted" message. A dead network path, a disarmed host,
and an operator denial were indistinguishable, which is exactly the
misdiagnosis behind the recent Android pairing support thread.

- core: new ungated `reject` module — shared close-code block 0x60–0x67
  (+ 0x42 busy promoted from the host), `RejectReason`, and
  `PunktfunkError::Rejected`; `pair()`/`connect()` decode the host's
  ApplicationClosed code into `Rejected` instead of a generic Io error.
  C ABI v7: status block −20…−28 and `punktfunk_connect_ex8` (`status_out`
  reports the failure cause; NULL-return alone can't). Wire unchanged —
  old peers see exactly the old bare close.
- host: every gate rejection `conn.close()`s with its typed code (and the
  human reason as close bytes) before erroring out of the session task.
- pf-client-core: shared `pair_error_message`/`connect_reject_message`
  wording consumed by the Windows + Linux + console-UI + CLI surfaces; a
  connect failure now renders the host's stated reason.
- android: `nativeTakeLastError()` JNI token + `ConnectErrors.kt` — a
  network timeout is no longer reported as "wrong PIN, or the host isn't
  armed", and a typed rejection skips the wake-and-wait fallback (the host
  is demonstrably awake).
- apple: `HostRejection` + `.rejected`; the pair sheet and session alerts
  show the stated reason; connect moves to `ex8`.

Completes the cross-client half of the hunks that rode along in 12148243
(client.rs / trust.rs / punktfunk1.rs) — main did not build without this.

Validated: workspace clippy -D warnings + full test suite green on .21
(EXIT=0, 309 host / 148 core suites); macOS core 147+c_abi green; swift
build green; Android Kotlin + native crate green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:58:43 +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 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 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 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 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 a2433d77cf fix(core): reordering no longer reads as packet loss — net late shards out of the loss estimate
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 53s
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 19s
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 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m35s
docker / deploy-docs (push) Successful in 25s
arch / build-publish (push) Successful in 11m22s
android / android (push) Successful in 14m48s
deb / build-publish (push) Successful in 14m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m12s
ci / rust (push) Successful in 18m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m11s
flatpak / build-publish (push) Successful in 5m59s
windows-host / package (push) Successful in 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m47s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m6s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
apple / swift (push) Successful in 5m12s
release / apple (push) Successful in 26m35s
apple / screenshots (push) Successful in 19m28s
Reversed/reordered delivery lets a FEC block reconstruct EARLY
(data + recovery >= k), counting still-in-flight shards into
fec_recovered_shards; window_loss_ppm then reported pure reordering as
loss, inflating LossReports — which size adaptive FEC and, since the
Automatic overhaul, feed the ABR controller (one severe window ends slow
start FOR GOOD, so a reorder burst could permanently kneecap a session's
climb).

Early reconstruct stays (it's the latency-right choice); the accounting
now nets it out. The reassembler counts a new fec_late_shards stat when a
parity-restored data shard ARRIVES after all — matched exactly: the
completed/abandoned-frame memory (ReassemblyWindow::completed, now a map)
remembers which shards each terminal frame reconstructed, and a late
arrival must match one (removed on hit), so wire duplicates of delivered
shards and stragglers of failed blocks count nothing. In-flight blocks
dedup via have_data. window_loss_ppm takes the late delta and estimates
from (recovered - late), saturating across window boundaries; both
callers (client core + probe) pass it.

The e2e reorder tests now assert the NET equals the true kill count in
both delivery orders, dup included (previously documented as a known
inflation). Not mirrored into the C-ABI PunktfunkStats — the loss windows
run in-core on every platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:59:17 +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 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 1a559e8d5e feat(core): scale the receive path to the new multi-Gbps ceiling
- REPLAY_WINDOW 32768 -> 131072: the anti-replay bitmap covered the
  120 ms loss window only to ~2 Gbps; the client now delivers ~4.8 Gbps
  wire, where a late-but-valid Wi-Fi-retried datagram would have been
  dropped as 'older than the window' — false loss. 16 KiB/session
  covers ~12 Gbps.
- RECV_BATCH 32 -> 128: syscall rate stays ~3.4k/s at 430k pkt/s and
  each pump iteration drains the kernel buffer deeper (ring 64->256 KB,
  client sessions only). flush_backlog's iteration cap rescaled to keep
  its ~190 MB guard equivalent.
- PUNKTFUNK_GSO gate is now value-aware: '=0' used to ENABLE GSO on
  Linux (presence check) while disabling Windows USO. GSO stays OPT-IN,
  deliberately: A/B'd twice today — it cuts send-thread CPU ~30% but
  its 16-packet line-rate trains cost delivered throughput on a
  constrained fabric (2.5GbE-hop pair: peak 2453 -> 1908 Mbps and 0.4%
  loss at a rate sendmmsg carries clean). Flipping the default belongs
  with pace-aware chunk spacing (plan Phase 1.2/1.3). docs-site row
  corrected to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:22:40 +02:00
enricobuehler ed0ce5dc6d feat(core): zero-copy pooled reassembly — shards land at their final AU offset
Rewrite the client Reassembler around one whole-frame buffer per frame:
frame_bytes rides in every header and packetize geometry is
deterministic (every non-final block is exactly max_data_per_block data
shards), so a data shard's final AU offset is computable on arrival —
copy it there once, straight from the decrypt ring. New
ErasureCoder::reconstruct_into decodes ONLY the missing shards directly
into the frame buffer's holes (gf16 native; gf8 legacy shim); received
recovery shards ride pooled shard-sized buffers. The completed buffer
IS Frame::data.

Deletes the per-shard to_vec + per-block concat + final AU concat
(~178k allocs and a double copy of every byte per second at 2 Gbps —
the pump wall the 2026-07-14 sweeps measured at 98.9% of an M3 Ultra
core). Reassembly now costs ~0.4 µs/packet in-stream.

The eager buffer changes the hostile-header exposure, so two new
firewalls: derived-geometry validation (a header lying about its
data_shards/block_count vs its own frame_bytes is dropped before it can
scribble across another shard's range) and an in-flight allocation
budget (IN_FLIGHT_BUF_FACTOR × max_frame_bytes) so a window of tiny
first-shards can't commit gigabytes.

Behavior parity pinned by the existing suite (all green unchanged) plus
new end-to-end roundtrips through the real Packetizer (multi-block +
partial tail, loss within budget, reversed delivery, duplicates, empty
frame, unrecoverable block ages out, budget enforcement). loss-harness
recovery curve identical; pipeline bench: gf8/1MB +42%, gf16 neutral
(host-encode dominated). Known pre-existing quirk kept as-is: reversed
delivery reconstructs early (data+recovery ≥ k) and counts late-not-lost
shards into fec_recovered_shards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:08:15 +02:00
enricobuehler 0058f624a2 feat(core): receive-path stage timing + frame-jitter observability (PUNKTFUNK_PERF)
Session::poll_frame accumulates per-stage ns (recv_batch syscall, AES-GCM
open, Reassembler::push incl. FEC) into a PumpPerf drained via
take_pump_perf(); the client pump logs the split plus completed-AU
inter-arrival jitter (p50/p95/max + late count) every report window.
Gated on PUNKTFUNK_PERF — one branch per stage when off.

Smoothness previously had no metric at all (jump-to-live counters fire
seconds late), and the receive core had no attribution. First live use
pinned the 1.57 Gbps client wall on software AES-GCM (7 µs/pkt) vs
0.4 µs reassembly — see punktfunk-planning/design/throughput-beyond-1gbps.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:07:10 +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 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 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