The Linux client already had WOL send + MAC storage + a Wake action + auto-wake-
on-connect, but the auto-wake just fired a packet and did one dial to the stored
address — so a host that woke on a new DHCP lease failed, and there was no
"waiting" feedback. Add the polished flow (mirrors Apple/Android HostWaker):
- ui_trust::wake_and_connect — send the magic packet, poll mDNS until the host
reappears (re-sending every 6 s, 90 s budget) behind a cancelable "Waking…"
dialog, then connect; if it woke on a new IP, re-key the saved host first.
- trust::rekey_addr — no-churn addr/port update keyed by fingerprint.
- the hosts page routes an offline saved-host-with-MAC tap to on_wake_connect
(the new flow) instead of fire-and-forget wake + immediate dial.
Builds + clippy + fmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each client learns a host's MAC from the mDNS `mac` TXT while it's awake, persists it on the saved-host record, and — when reconnecting to an offline host — sends a magic packet before connecting, plus an explicit "Wake host" action. Apple wraps the C-ABI; linux/windows call the core fn directly (linux also gains a --wake CLI mode); android via a new nativeWakeOnLan JNI export (the mDNS browse record gains a 7th mac field); decky shells out to the linux client's --wake before launching the stream.
iOS/tvOS need the managed com.apple.developer.networking.multicast entitlement (pending Apple approval), so the wake path + UI are gated off via PunktfunkConnection.wakeOnLANAvailable and the entitlement is commented out — keeping iOS/tvOS releasable. MAC-learning stays active on every platform so it lights up the moment it's ungated. macOS works today.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-caused fixes from on-Deck testing (owner + first external tester):
- System input broke while the app was merely OPEN: SDL's Steam Deck HIDAPI
driver clears the built-in controller's "lizard mode" (trackpad-mouse,
clicky pads) at device ENUMERATION and keeps feeding the firmware watchdog
(SDL_hidapi_steamdeck.c InitDevice/UpdateDevice) — and we enabled that
driver at startup and held every pad open app-lifetime. The Valve HIDAPI
hints are now enabled only while a session is attached, and only the active
pad is opened (Settings enumerates via SDL's ID-based metadata getters, no
open). Close/detach hands the hardware back; the watchdog restores lizard
mode within seconds. This also unblocks click-to-capture on the Deck (the
dead trackpad made "input not passed through" a symptom, not a cause).
- Washed-out colors from a Windows host with an HDR desktop: the host ships
Main10 BT.2020 PQ IN-BAND (correct VUI) while the Welcome still says SDR;
this client rendered everything as BT.709 narrow. Colour signaling is now
read per-frame (video::ColorDesc from the AVFrame CICP fields) and drives
the GdkDmabufTexture color state, the software path's swscale matrix/range
plus a tagged MemoryTexture for PQ, and an "· HDR" HUD chip — GTK tone-maps
correctly on SDR displays, mid-session SDR↔HDR flips included. Regression-
tested against a checked-in Main10 PQ fixture (tests/pq-frame.h265).
- Streams start fullscreen by default (Settings toggle; F11 / the controller
chord lead out, and the pointer at the top edge reveals the header while
input isn't captured — a Deck desktop has no F11). Gaming-Mode launches
(--fullscreen / Deck env) build the stream page with NO header bar at all:
gamescope doesn't reliably ACK xdg_toplevel fullscreen, so anything keyed
on is_fullscreen() could leave the title bar drawn over the stream.
- Game Mode settings were uneditable: GTK popovers are xdg_popups, which
gamescope never maps for nested apps — every ComboRow dropdown flashed and
died. Under gamescope the preferences dialog now uses in-window selection
subpages (PreferencesDialog::push_subpage) via a ChoiceRow that stays a
stock ComboRow on desktops. Covered by an in-process GTK test
(choice_row_modes, #[ignore]d — needs a display).
- Forwarded-controller pin persists across restarts (Settings::forward_pad,
stable vid:pid:name key — SDL instance ids are per-run) and survives
disconnects; automatic selection skips Steam Input's sensor-less virtual
pad (28de:11ff) so gyro doesn't silently die on Bazzite/Deck.
- "Punktfunk" branding in the About dialog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- library.rs + ui_library.rs: the host's unified game library over the
management API (the Apple LibraryClient/LibraryView ported) — mTLS with the
paired identity, host verified by its pinned cert fingerprint (ureq + rustls,
unified with the workspace rustls 0.23); posters load async with monogram
placeholders, and picking a title starts a session that asks the host to
launch it (the library id rides the Hello).
- app.rs (~800 lines lighter) splits into cli.rs (argv/headless
pairing/--connect/screenshot scenes), launch.rs (mode resolve + session
worker + event stream into the UI) and ui_trust.rs (TOFU / SPAKE2 PIN /
delegated-approval dialogs); ui_hosts/ui_stream reworked around the split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)
**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
`video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
(drift-guarded against the wire values). Header regenerated.
**Host:** passes `hello.preferred_codec` into `resolve_codec`.
**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.
**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).
Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds negotiated 5.1/7.1 surround to the punktfunk/1 protocol and every client
(previously stereo-only):
- core: new shared `audio` layout table (LAYOUT_51/71 + identity multistream
mapping, canonical wire order FL FR FC LFE RL RR SL SR); Hello/Welcome
`audio_channels` negotiation via the trailing-byte back-compat pattern (old
peers fall back to stereo); C-ABI `punktfunk_connect_ex6`,
`punktfunk_connection_audio_channels`, and in-core multistream decode
`punktfunk_connection_next_audio_pcm` for embedders without a multistream
Opus decoder. Real-libopus channel-identity round-trip test.
- host: native audio thread captures + Opus-(multi)stream-encodes at the
negotiated count (with a cross-session cached-capturer channel-mismatch fix);
GameStream surround unified onto the safe `opus::MSEncoder`, dropping
`audiopus_sys` (~4 unsafe blocks) and un-gating Windows GameStream surround;
WASAPI loopback capture relaxed to 2/6/8 with the correct dwChannelMask.
- clients: Linux (PipeWire), Windows (WASAPI), Android (AAudio) decode via
`opus::MSDecoder` + render multichannel; Apple decodes in-core to PCM →
AVAudioEngine with an explicit wire-order channel layout; each gains a
Stereo/5.1/7.1 setting. `punktfunk-probe --audio-channels N` is the headless
validator.
Verified on Linux: core/host/linux/probe test suites + the Android Rust
(cargo-ndk) build, clippy -D warnings, and rustfmt all green. Windows/Apple
builds, all on-glass checks, and the live native loopback are pending (CI / a
free box).
Also lands the concurrent in-tree HEVC 4:4:4 host work (PUNKTFUNK_444): it
shares the same touched files (quic.rs, punktfunk1.rs, encode/*, ...) and so
cannot be committed separately from the surround changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plugin self-update (no Decky store): CI publishes a per-channel manifest.json
({version, immutable per-version artifact, sha256}) beside the zip and bakes
update.json {channel, manifest} into the plugin. main.py `check_update` reads the
installed version from package.json (the value Decky reports — not plugin.json),
fetches the channel manifest, and the frontend shows an "Update to vX" button that
drives Decky Loader's own install RPC (root downloads + SHA-256-verifies + hot-reloads).
CI now stamps a plain-numeric semver (0.3.<run> canary / X.Y.Z stable) into
package.json — a -ciN suffix would mis-order under compare-versions.
Linux client: `--fullscreen` (plus SteamDeck/gamescope env fallback) enters GTK
fullscreen on stream start so Gaming-Mode chrome is hidden; native-mode resolution
falls back to the display's first monitor when the window isn't mapped yet (was
dropping to the 1080p floor — wrong on the Deck's 1280×800); add a confirmed
"Remove saved host" action (KnownHosts::remove_by_fp).
Docs: new docs/steam-deck.md (Decky install/pair/stream/self-update/troubleshooting),
wired into meta.json nav, and cross-linked from clients/install-client/channels. This
is the page docs.punktfunk.unom.io/docs/steam-deck — the website's download link
pointed at it before it existed; committing it makes that link resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bodies of work in one commit (the rename moved files the fixes also touched).
Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
clients/probe (crate punktfunk-probe), client-linux->clients/linux,
client-windows->clients/windows, punktfunk-android->clients/android/native
(crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
kept only in docs/implementation-plan.md. docs/m2-plan.md->
docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.
Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
tracked it for the client keyframe-recovery loop; it was never reachable from
the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
frames_dropped and request a keyframe when it climbs -- the same loss-driven
recovery Linux/Windows already had. Under infinite GOP the decoder silently
conceals reference-missing frames, so the decode-error trigger rarely fires.
Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
interruption / server reset) and drop the permanent `broken` latch on a
transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.
Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>