bf9be59f0b462a01dce9d3441466a96caee0746c
82 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3332aedae |
fix(clients/apple): return to windowed after a stream error, not just an active disconnect
apple / swift (push) Successful in 1m12s
release / apple (push) Successful in 8m48s
apple / screenshots (push) Successful in 5m48s
android / android (push) Successful in 4m6s
arch / build-publish (push) Successful in 7m3s
ci / rust (push) Successful in 7m1s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m0s
deb / build-publish (push) Successful in 3m40s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 5m4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m51s
docker / deploy-docs (push) Failing after 18s
A session error tears down the same as a deliberate disconnect (connection → nil flips FullscreenController's `active` off), but ALSO sets `errorMessage`, raising the "Connection failed" alert. On macOS that alert is a window sheet, and AppKit drops `-toggleFullScreen:` while a sheet is attached — so the exit was swallowed and the window stayed fullscreen on the home screen (an active disconnect sets no error, so it never stuck). Defer the alert while a forced-fullscreen exit is pending: no sheet attaches during the exit, the window returns to windowed, then the alert presents over it. Not gated when fullscreen is the user's own manual choice (opt-out setting), where nothing is auto-exiting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
069fcdfca6 |
feat(clients/apple): user-configurable Allow VRR (macOS + iOS)
Add an "Allow VRR" toggle (Settings → Display; default on, non-tvOS) that hands the presenter's display link a wide frame-rate RANGE — preferred = the stream rate — so a ProMotion / adaptive-sync display can vary its physical refresh to match the stream. `SessionPresenter.syncFrameRate` now runs on macOS too: on it requests min 24 / max display / preferred stream Hz; off restores the prior behavior (macOS free-runs at the native rate, iOS keeps its 30 Hz floor). A no-op on fixed-refresh displays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f58d91ba19 |
polish(clients/apple): tighten settings copy, fix stale ⌃⌥⇧S shortcut
The Statistics footer still advertised the old ⌘⇧S toggle after the cross-client shortcut migration — corrected to ⌃⌥⇧S (StreamCommands). Split the Audio footer into a platform-aware string (the "System default follows device changes" note only applies to macOS, where the device pickers live). Trim the overloaded Video-quality, Controllers, gamepad-UI, touch/pointer, presenter and library footers to the load-bearing facts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
01428ced58 |
feat(clients): reachability-probed presence + shareable Decky host management
apple / swift (push) Failing after 27s
release / apple (push) Failing after 26s
apple / screenshots (push) Has been skipped
windows-host / package (push) Has been cancelled
arch / build-publish (push) Has been cancelled
android / android (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
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
flatpak / build-publish (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-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) Has been cancelled
decky / 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
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
Add a bounded, trust-agnostic, mDNS-INDEPENDENT QUIC reachability probe and surface it everywhere saved-host presence is shown, so a host reached over a routed network (Tailscale/VPN/multicast-filtering LAN) no longer reads Offline just because it isn't advertising — the display-side companion to the 0.8.4 dial-first connect fix. Core: - punktfunk-core: NativeClient::probe (bounded handshake; a real host answers even on trust mismatch, a wrong/closed/TCP-only port fails) + punktfunk_probe C ABI (ABI_VERSION 3->4, header regenerated). - pf-client-core: trust::probe_reachable_many (parallel per-host sweep). Presence pips now read `advertising OR probed-reachable`, refreshed by a ~10-12s background sweep off the UI thread: - Linux (relm4): ui_hosts probed map + HostsMsg::Probed sweep. - Windows (windows-reactor): pf-probe worker -> HostsProps.probed. - Apple (SwiftUI): HostStore.refreshReachability, driven by HomeView + GamepadHomeView .task. - Android (Compose): nativeProbe JNI seam + periodic LaunchedEffect (LNP-gated), online dot added to the touch HostCard. - Decky already probes via --list-hosts --probe. Decky client: make the flatpak client's known-hosts store the single source of truth via new headless CLI modes (--list-hosts / --add-host / --set-host / --forget-host / --reset / --reachable). The plugin can now add a host by address, edit/forget hosts, reset all state (keeping the client identity), and shows probe-backed online pips — state is shared with the desktop client, not duplicated. Also lands in-progress Android 17 LNP groundwork (targetSdk 37 + ACCESS_LOCAL_NETWORK runtime flow, permission dialogs) that was already present in the working tree. Verified: cargo check + clippy clean (punktfunk-core, pf-client-core, linux, android native); android assembleDebug BUILD SUCCESSFUL; decky typecheck + rollup build clean; probe true/false-positive behaviour exercised against a live host. Windows and Apple were not compiled locally (no MSVC/Xcode on this Linux box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6198da3daf |
feat(clients/apple): cross-client shortcuts + start banner, opt-in V-Sync, presenter rework
apple / swift (push) Failing after 28s
release / apple (push) Failing after 22s
apple / screenshots (push) Has been skipped
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (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
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
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
Align the macOS/iPad Stream menu on the cross-client Ctrl+Alt+Shift set the Windows and Linux clients reserve — Release Mouse (⌃⌥⇧Q), Disconnect (⌃⌥⇧D), HUD toggle (⌃⌥⇧S) — with ⌘⎋ kept as the macOS/iPad capture toggle, and surface them on a 6-second banner at stream start. Add an opt-in V-Sync present mode (punktfunk.vsync, default OFF = lowest-latency immediate present; PUNKTFUNK_PRESENT_MODE overrides for A/B), with the presenter reworked to a frame-arrival-triggered render thread across Stage2Pipeline / MetalVideoPresenter / SessionPresenter, plus the windowed title-bar safe-area handling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
838a1239cf |
feat(codec): make client codec selection real — GPU-aware native advertisement
apple / swift (push) Successful in 1m7s
release / apple (push) Successful in 8m27s
windows-host / package (push) Successful in 7m59s
apple / screenshots (push) Failing after 2m38s
android / android (push) Successful in 4m44s
ci / rust (push) Failing after 46s
arch / build-publish (push) Successful in 5m32s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 1m2s
deb / build-publish (push) Successful in 4m44s
ci / bench (push) Successful in 5m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
decky / build-publish (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 11m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m14s
docker / deploy-docs (push) Successful in 13s
The clients' codec pickers sent a preference the host threw away: host_wire_caps() hardcoded HEVC for every GPU backend, so the native path never emitted H.264/AV1 regardless of what the user chose. Host: advertise what the backend actually encodes, mirroring the GameStream serverinfo logic — software openh264 emits H.264; probed backends (Linux VAAPI, Windows AMF/QSV) advertise their per-GPU CodecSupport via the new wire_mask(); NVENC keeps the Moonlight-validated H.264|HEVC|AV1 static superset; an empty probe falls back to the superset so auto clients still resolve HEVC. Gate 10-bit to HEVC (like the 4:4:4 gate) now that a client can steer the codec — Main10 is the only 10-bit encode path. Fix the web-console stats label hardcoded to "hevc" (new Codec::label(), shared with the GameStream register_session mapping). Android: replace the hardcoded H264|HEVC Hello advertisement with a videoCodecs param fed by VideoDecoders.decodableCodecBits() (AV1 bit only when a real hardware video/av01 decoder exists — the decode loop was already mime-driven); offer AV1 in the codec picker on capable devices. Decky: add the missing "Video codec" dropdown (auto/hevc/h264/av1) to the plugin settings, round-tripping the same codec key the flatpak client reads. Apple: unchanged by design (AnnexB.swift is NAL-only, AV1 is never advertised); refresh the stale "hosts don't emit AV1 on the native path yet" comments here and host-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ab6790ef6c |
fix(clients): dial-first reconnect — don't gate the connect on mDNS presence
A saved host that stopped advertising on mDNS but has a learned Wake-on-LAN MAC was routed into a wake-and-WAIT-for-mDNS flow instead of being dialed. For a host reached over a routed network (Tailscale/VPN/another subnet) mDNS never sees it, so the wait could never succeed and reconnects were impossible — the host log showed no connection attempt at all. The tile's Offline pip and this connect gate shared the same LAN-only `advertises`/`liveAdvert` predicate, so a perfectly reachable host read as unreachable. The first connect worked only because the MAC hadn't been learned yet (a direct dial); once learned, every reconnect wedged in the waker. All four full clients carried this pattern (deliberate mirrors of the Apple `HostWaker`). Each now DIALS FIRST: fire the magic packet up front (fire-and-forget — harmless if the host is awake, and a genuinely-asleep box is booting while the dial times out) and attempt the connection unconditionally. Only a FAILED dial falls into the visible "Waking…" wait, whose redial carries no fallback so it can't loop. A fingerprint-mismatch/trust-rejection failure means the host ANSWERED, so it never takes the wake path. - apple: SessionModel.connect gains `onUnreachable`; startSession dials first. - linux (relm4): AppMsg::WakeConnect wakes+dials; a per-request wake_fallback is armed and consumed in SessionExited, matched by fingerprint/address. - windows: initiate_waking + ConnectOpts.wake_on_fail; the worker's Failed arm escalates to wake_and_connect only on a non-trust failure. - android: doConnectDirect gains onFailure; doConnect wakes+dials first. Decky was already correct (always launches `--connect`; WoL fire-and-forget). Explicit "Wake host" menu actions are unchanged — waiting on mDNS is right when the user explicitly asked to wake a box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
91fadce900 |
feat(clients): signal explicit exit (QUIT_CLOSE_CODE) on deliberate disconnect
The core's deliberate-quit close (NativeClient::disconnect_quit → QUIT_CLOSE_CODE,
host skips the keep-alive linger) was implemented but never called by any client.
Wire it to each client's explicit user-disconnect action — NOT to a network drop /
host-ended / app-background (those keep the linger for a reconnect):
- core: new C-ABI punktfunk_connection_disconnect_quit(c) for the ABI clients
- Linux (direct-core): Ctrl+Alt+Shift+D + the controller escape chord
- Windows (direct-core): Ctrl+Alt+Shift+D
- Apple (C-ABI): PunktfunkConnection.disconnectQuit() + a `deliberate` flag on
SessionModel.disconnect() (sessionEnded passes false → keeps the linger)
- Android (JNI): new nativeDisconnectQuit export, called from the back gesture +
the Select+Start+L1+R1 chord (not the host-gone watchdog)
- probe already did this via --quit (
|
||
|
|
60121f1ea9 |
feat(apple): wake-until-up overlay + host edit with MAC prefill
- HostWaker + WakeOverlay: after sending the Wake-on-LAN packet, wait until the host is really back (resend + mDNS poll, timeout, cancel/retry) before connecting. macOS-only in practice — WoL stays gated off on iOS/tvOS pending the multicast entitlement. - Add/Edit host sheet gains a Wake-on-LAN MAC field, prefilled from the stored MAC or the live mDNS advert; parseMacs validates aa:bb:cc:dd:ee:ff. - Gamepad chrome/home and glass-style polish. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fb709d68ce |
feat(clients): Wake-on-LAN in apple/linux/windows/android/decky
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> |
||
|
|
e56470cf56 |
fix(apple-client/audio): capture the right channel of a multi-channel mic + diagnostics
The mic uplink handed the host pure digital silence on a multi-channel interface: AVAudioConverter's N→stereo downmix takes channels 0/1, but a pro interface puts the mic on ONE higher discrete channel. Fold the input to a mono bus ourselves instead — pick the mic's channel (or sum all) and resample that to the encoder's 48 kHz stereo, so the silent 0/1 downmix never happens. - New "Microphone channel" setting (macOS): Auto (sum every channel — a lone hot mic passes at full level) or pin 1-based channel N. Picker appears only for multi-channel devices, driven by the device's input channel count. - Diagnostics that make this class of failure self-naming next session: log the actual live capture device + format + fold mode, warn on a silent UID fallback, and a one-shot silence tripwire on the EXTRACTED signal (WARN on 10 s of zeros, else peak dBFS). - foldToMono extracted as a pure, unit-tested helper (pin / sum-clamp x interleaved / deinterleaved / mono / out-of-range). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d6dbce1dcc |
feat(clients): host/network split in every stats HUD (stats phase 2, client side)
Consumes the 0xCF host-timing plane (
|
||
|
|
83460cd2c0 |
feat(clients): unified stats vocabulary across every client + Moonlight comparison docs
One stat model everywhere (design/stats-unification.md): four measurement points (capture/received/decoded/displayed), three stages that tile the interval exactly, and a HUD that shows the addition explicitly — end-to-end 14.2 ms p50 · 19.8 p95 · capture→on-glass = host+network 9.8 + decode 2.1 + display 2.3 replacing each client's ad-hoc mix of overlapping absolutes (the Apple HUD's three arrow lines that looked sequential but weren't), mean-vs-median decode times (Windows/Linux), missing same-host-clock flags (Windows/Linux), and three different names for the same capture→received measurement (probe's "reassembled", Apple/Android's "client", Windows/Linux's post-decode "lat"). Per client: Apple threads receivedNs through the VT decode via the frame refcon bit pattern so the decode stage exists at all (stage-1 fallback honestly degrades to a capture→received headline); Windows carries FrameTimes through the existing frame channel to the render thread and adds e2e p50/p95 post-Present; Linux stamps received at AU pop and rides decoded_ns on DecodedFrame to the paintable-set site; Android pairs receipt stamps with MediaCodec output buffers via the codec's pts round-trip (JNI stats array 14→16 doubles, indexes 0-13 unchanged). fps now uniformly counts received AUs; lost/(received+lost) per window, hidden at zero. docs-site gains "Understanding the Stats Overlay": what each line means, why the equation only approximately sums (percentiles), and a line-by-line Moonlight/Sunshine matrix — including that Moonlight has no end-to-end number and its "network latency" is an ENet control RTT, so punktfunk's headline must not be compared against any single Moonlight line. Verified here: linux client + probe + core check/clippy/fmt green, android native cargo-ndk arm64 check green. Pending: Windows CI + on-glass, swift test on the mac, on-device Android. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b5fe2a783d |
fix(apple/ios): the ACTUAL type-checker bomb was pointerSection's footer ternary chain
|
||
|
|
844372b203 |
fix(apple/ios): split streamModeSection — the inline iOS branch blew the type-checker budget
The Section's iOS content (resolution wheel + 3-way refresh rows + bitrate rows) as ONE ViewBuilder expression hit "the compiler is unable to type-check this expression in reasonable time" — failing exactly one build slice, the iOS archive, so swift test (macOS) and the tvOS/macOS archives never saw it and the 0.6.0 iOS TestFlight upload soft-failed. Extracted iosResolutionWheel / iosRefreshRows / bitrateRows; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7e2d7c893c |
feat(apple,android): three-way touch input — trackpad cursor (default), direct pointer, real multi-touch passthrough
The two touch clients had exactly complementary gaps: iOS forwarded fingers ONLY as raw wire touches (no way to drive the host cursor from the touch screen), Android had the two mouse modes but no passthrough. Both now share one three-way "Touch input" setting: Trackpad (default) / Direct pointer / Touch passthrough. iOS/iPadOS: Input/TouchMouse.swift ports the Android gesture engine 1:1 (same px-based acceleration curve; tap=click, two-finger tap=right-click, two-finger drag=scroll, tap-then-drag=held drag, three-finger tap=stats HUD via the shared hudEnabled default); direct-pointer mode maps through the aspect-fit letterbox; the previous always-on behavior lives on as the passthrough option. The mode latches per gesture (a Settings change never splits one gesture across models), touchesCancelled releases held state without synthesizing a click, and session stop flushes a mid-drag button. Settings picker on iPhone + iPad next to the iPad-only pointer-capture toggle. Deliberate default change: trackpad, not passthrough. Android: new nativeSendTouch JNI shim → wire TouchDown/Move/Up (the host already injects real touch on every backend — libei touchscreen, wlroots, KWin fake-input, SendInput); streamTouchPassthrough forwards every finger with stable ids and lifts still-held contacts on teardown; the trackpadMode Boolean becomes the TouchMode enum (old pref migrated on load, never rewritten) with a Settings dropdown. Verified: macOS swift build + full suite (incl. new TouchMouseTests), iOS Simulator Swift compile, cargo check/fmt/clippy on the native crate, Kotlin app+kit compile + unit tests. On-glass feel of the iOS ballistics and Android passthrough against a touch-aware app still pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1b9d81928e |
feat(apple/gamepad): rewrite rumble renderer — bounded divergence + iOS 27 plain-player fix
Ground-up RumbleRenderer rewrite around one principle: rumble is idempotent state on a lossy channel, and the actuator's divergence from it must be bounded, not best-effort. The old renderer rebuilt an infinite-duration CHHapticAdvancedPatternPlayer per 0xCA datagram via an async stop; one stop lost inside CoreHaptics left an unstoppable player buzzing forever (the "entered the menu and rumble never stopped" bug). - Finite 4 s segments, never infinite events — a leaked player self-silences; steady levels re-arm seamlessly ON the engine timeline (no stop/start race) - GamepadFeedback drains the rumble plane DRY per cycle, newest-wins (was one datagram per 8 ms through a 16-deep drop-newest queue = lag + shed stops) - Host 500 ms state refreshes dedupe to a liveness stamp; zero applies immediately; nonzero ramps throttle to one rebake/25 ms per motor - Throwing player stop escalates to engine.stop() (kills leaked players); 1.6 s staleness watchdog (Policy.session) force-silences on a dead channel; the test panel holds levels via Policy.manual - Plain makePlayer, NEVER makeAdvancedPlayer: gamecontrollerd's controller haptics server advertises `adv players: 0`, and iOS 27 beta 2 hard-drops advanced loads with an XPC decode fault (-4811/4097, rumble silently dead). Live-verified on an iOS 27 beta 2 iPhone: DualSense rumble works - Split-handle engines fall back to one combined .default engine on repeated failure; renderer publishes health transitions and the test panel shows them (a refused system service no longer reads as silent app breakage) - Per-motor sharpness on split handles (0.3 heavy / 0.7 light); macOS DualSense raw-HID path gains a ~1 s keepalive re-write while nonzero - RumbleTuningTests pin the scheduling math, tuning relations, and a queue/ticker teardown smoke test Stuck-rumble streaming repro revalidation on glass still pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
34fe896ade |
feat(apple): gamepad UI v2 — controller settings + add host, aurora, macOS
Sources reorganized (client: Home/Session/Settings/Stores/Support/Trust; kit: Audio/Connection/Gamepad/Input/Support/Video/Views) with the big files split along the same seams. The gamepad mode is couch-complete, and now on macOS too (the living-room Mac case), not just iOS/iPadOS: - GamepadSettingsView: a console-style, fully controller-navigable settings screen (X from the launcher) — up/down moves focus, left/right steps values (clamped, boundary thud), A cycles/toggles, B closes; the focused row shows a one-line description. Backed by GamepadMenuList, the vertical sibling of GamepadCarousel, and SettingsOptions — the option lists hoisted out of SettingsView statics and shared by the touch, tvOS and gamepad settings. - GamepadAddHostView + GamepadKeyboard: register a host end to end with a pad — field rows open an on-screen controller keyboard (dpad grid, A types, X backspaces, B done); the launcher carousel ends in an Add Host tile, so the dead-end "add one with touch first" empty state is gone. - Launcher polish: contextual hint bar with the pad's real button glyphs, controller name + battery chip, one shared console chrome. - GamepadScreenBackground: an animated aurora (TimelineView-driven drifting blobs in the brand's violet family, breathing radii, slow hue shift, legibility scrim; freezes under Reduce Motion). Pure SwiftUI on purpose — a .metal library only bundles reliably in one of the two build systems (SPM vs the xcodeproj's synced folders) these sources compile under. - macOS port: settings/add-host/library present as sized sheets (a macOS sheet takes its content's IDEAL size, and the GeometryReader-driven screens collapsed to nothing), NSScreen-based mode lists, scroll indicators .never (the "always show scroll bars" setting overrides .hidden), tray scrims so scrolled rows dim under the pinned title/hints, extra title clearance, and a PUNKTFUNK_FORCE_GAMEPAD_UI=1 dev hook — launcher/settings/add-host/keyboard/ library render-verified live on a real Mac + LAN hosts. - GamepadMenuInput: X button support, and (re)start now snapshots held buttons so a controller handoff press never fires twice (the B that closed the keyboard no longer also cancels the screen underneath). - Cleanups: one "Connection failed" alert in ContentView instead of one per home screen; HostDiscovery.advertises/unsaved shared by both home screens. - host: can_encode_444 stub for the non-Linux/Windows host build (the macOS synthetic-source loopback used by the Swift tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b357934cb1 |
feat(clients): codec preference on Windows/Apple/Android clients (Phase 2b)
Rounds out codec negotiation across the last three clients — each advertises what it can decode, builds its decoder from the resolved Welcome.codec, and exposes a "Video codec" preference picker. **Windows** (Rust, mirrors Linux): `decodable_codecs()` + `ffmpeg_codec_id()`; the D3D11VA and software FFmpeg decoders (and the mid-session D3D11VA→software demotion) open the negotiated codec instead of hardcoding HEVC; settings gain a `codec` field + reactor ComboBox; `--codec` CLI flag. **Apple** (Swift/C-ABI): AnnexB is now codec-aware — a `VideoCodec` enum drives H.264 vs HEVC NAL parsing / parameter-set extraction (`CMVideoFormatDescriptionCreateFromH264ParameterSets` for H.264, no VPS) and AVCC repacking; `PunktfunkConnection` advertises H264|HEVC via `punktfunk_connect_ex7`, reads `resolvedCodec` (`punktfunk_connection_codec`), and threads `videoCodec` into the stage-1/2 pipelines + `VideoDecoder`; SettingsView "Video codec" Picker (auto/HEVC/H.264). AV1 is left out (hosts don't emit it on the native path, and it's not an AnnexB codec). Test call sites updated. **Android** (Kotlin + Rust JNI): the JNI `nativeConnect` gains `preferredCodec`; the native decode loop picks the AMediaCodec MIME (`video/hevc`|`video/avc`) from `connector.codec` and advertises H264|HEVC; Settings `codec` field + Compose dropdown. Core/host/probe/Linux clippy + tests green (unchanged from 2a). Windows/Apple/Android compile on their platform CI (this Linux box can't build them — Windows toolchain / Xcode / the Android NDK's opus-cmake toolchain). All follow the Linux client's validated pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
546b178ecc | feat(apple): gamepad ui | ||
|
|
ae51276a03 |
feat(web): consolidate paired devices, self-contained sections, docs + lint
Web console - Pairing/Library/Stats refactored into self-contained subsections that each own their own queries + mutations; a shared slot-based layout (view.tsx) is filled by the live page (containers) and Storybook (pure cards + fixtures) so the layout can't drift. - All paired devices in one list on Pairing with a protocol column (punktfunk/1 + Moonlight), routing each unpair to the right endpoint; the redundant Clients page is removed. - Library: overview grid split from the add/edit form into separate files. - Login screen links out to the docs. Docs - "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows) plus a new "Forgot your Password?" troubleshooting page, linked from the login screen. - Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the Bun entry, launchers, systemd units, and packaging. Tooling - Biome now respects .gitignore (stops linting generated code), config migrated to 2.5.1; all lint issues fixed cleanly. Also includes this branch's in-progress host, Apple client, packaging, and CI changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ae1b392011 |
feat(apple): Improve presenter
web-screenshots / screenshots (push) Has been cancelled
linux-client-screenshots / screenshots (push) Has been cancelled
deb / build-publish (push) Has been cancelled
android / android (push) Has been cancelled
android-screenshots / screenshots (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, 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 (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (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
feat(apple): add cursor capture on iPad |
||
|
|
8c6c9705f4 |
feat(apple): stage-2 default + pixel-perfect, decode robustness, UI/rumble polish
Stream reliability - Default to the stage-2 presenter (VTDecompressionSession + CAMetalLayer): it detects and recovers a wedged decoder, where stage-1's AVSampleBufferDisplayLayer freezes hard on a lost HEVC reference frame with no app-side recovery (confirmed Apple limitation). Stage 1 is now a DEBUG-only presenter toggle, plus the automatic no-Metal fallback. - Stage-2 pixel-perfect: render the drawable at the decoded size (shader stays 1:1 = identity) and let the layer's contentsGravity scale via the system compositor — the same path stage-1's videoGravity used — instead of scaling in-shader. - Loss recovery in both pumps is now a persistent awaitingIDR want, retried until an IDR actually lands, so a keyframe request swallowed by the throttle can't strand a frozen frame; 100 ms keyframe throttle to match the Android path. - Fix "Publishing changes from within view updates": defer the HostStore writes out of the .onChange(of: model.phase) callback. - Move AVAudioSession setActive/setCategory off the main thread (async on a shared serial queue) to stop the UI-stall warning. Controllers - Rumble: capped-exponential backoff when the gamecontrollerd.haptics XPC breaks (-4811) so a transient server interruption self-heals instead of cascading; playsHapticsOnly so a controller engine doesn't join the always-active streaming audio session. - Host cards: iPad pointer "magnet" hover effect; iPhone press scale + light haptic. UI / design - Ship Geist (SIL OFL 1.1) as the app font (bundled OTFs + registration), with the license surfaced in Acknowledgements. - Restructure iOS/iPadOS Settings into a category NavigationSplitView; resolution wheel with custom-resolution entry; 10-bit HDR toggle in Display. - Industrial host-card redesign (left-aligned, bold, brand monogram tiles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
77c244b208 |
fix(apple): render Acknowledgements notices in lazy chunks
THIRD-PARTY-NOTICES.txt is ~885 KB / 16k lines; rendering it in a single SwiftUI Text overshot the text-rendering height limit — it laid out for ages and drew blank below the cutoff (only the small punktfunk licenses above it showed). Split the notices into ~80 line-chunks (<=200 lines / <=18 KB each, computed once as Licenses.thirdPartyNoticesChunks) and render them in a top-level LazyVStack so only on-screen chunks lay out and no chunk is tall enough to clip. Chunking is lossless — rejoining the chunks reproduces the original byte-for-byte, so no notice text is dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eed1404faf |
fix(apple): stop the iOS/iPadOS Add Host sheet from scrolling
The add-host content is a SwiftUI Form (backed by a scrollable list), so it bounced/scrolled inside the fixed .height(320) detent even though the three rows + action button fit exactly. Lock it with .scrollDisabled(true) on iOS (covers iPadOS); macOS (fixed-size panel) and tvOS (custom rows, no Form) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a05c08b1b0 |
feat(clients): in-app OSS / third-party-license screens
Surface THIRD-PARTY-NOTICES.txt in every GUI client (the desktop packages already
ship it as a file; this adds the on-glass screen):
- Linux: Preferences -> About -> Third-party licenses (adw::AboutDialog with the app
license + Legal sections; include_str! the root notices).
- Apple: macOS About tab / iOS+tvOS Acknowledgements link; notices bundled as
PunktfunkKit SPM resources, read via Bundle.module (the Xcode app links the SPM
product, so they ride along - no .pbxproj edit).
- Android: Settings -> About -> Open-source licenses (reads the bundled asset).
- (Windows landed earlier in
|
||
|
|
624387ea82 |
feat(pairing): seamless no-PIN delegated approval (host parks the knock, clients add "Request access")
Web-console "Approve" (delegated pairing, roadmap §8b-1) was unreachable: every client routed a fresh pair=required host straight to the SPAKE2 PIN ceremony, so no "knock" was ever recorded; and an unpaired connect was rejected+closed with no way to resume after approval. The backend + console were complete but had no client-side trigger and no post-approval admit path. Host (native_pairing.rs, punktfunk1.rs): an unpaired identified knock is now PARKED instead of rejected — it releases its NVENC session permit, awaits an operator decision (NativePairing::wait_for_decision, woken by a Notify on approve/deny), and on approval re-acquires a slot and admits the SAME connection with no reconnect. QUIC keep-alive (4s/8s) holds the parked connection warm. The pairing gate moves out of the HANDSHAKE_TIMEOUT-bounded handshake future; approve_pending is reordered read-then-add and wait_for_decision double-checks is_paired to close a "neither pending nor paired" race. New PENDING_APPROVAL_WAIT (180s). Tests: delegated_approval_admits_after_knock now approves mid-park (no reconnect) + new wait_for_decision_approve_deny_timeout unit test (108 host tests green). Clients (Linux/Apple/Windows/Android): a fresh pair=required host now offers "Request access" alongside the PIN ceremony — a plain identified connect with a ~185s handshake budget and a cancelable "waiting for approval" UI; on success the host is saved as paired, and cancel returns the UI immediately while a late- resolving connect is torn down silently via a per-attempt flag. Apple reuses the existing C-ABI timeout_ms (no ABI change); Windows adds SessionParams.connect_timeout + a RequestAccess screen; Android adds a timeoutMs arg to the nativeConnect JNI seam (both sides + both callers). Linux built + clippy + fmt clean; Apple/Windows/ Android pending their CI/on-device compiles. SPAKE2 ceremony reviewed end-to-end against the spake2 0.4 contract — correct, no changes needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7612238a59 |
feat(audio): end-to-end 5.1/7.1 surround across the native path + all clients
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> |
||
|
|
93273a24b4 |
feat(apple): App Store screenshot harness + CI zip artifact
A DEBUG-only "shot mode" renders one mock-populated screen full-bleed (PUNKTFUNK_SHOT_SCENE=<name> -> ScreenshotHostView instead of ContentView), so the OS can screenshot the REAL, fully-rendered UI. tools/screenshots.sh drives it: screencapture for the mac window, `simctl io booted screenshot` for the iOS/iPad/tvOS Simulators, at exactly the App Store Connect sizes. ImageRenderer was tried first and rejected: it can't rasterize this app's chrome (NavigationStack, Form/TabView, Liquid-Glass/NSVisualEffect all render black or the "can't render" placeholder). Capturing the live window/Simulator avoids that. Only the stream hero is synthetic (StreamView needs a live connection) - a synthwave frame + the real glass HUD, overridable via PUNKTFUNK_SHOT_HERO. CI: a new `screenshots` job in apple.yml builds the iOS (+ tvOS best-effort) xcframework slices, runs the harness per platform best-effort, and attaches the result as a single zip artifact (punktfunk-appstore-screenshots). It is isolated from the build/test job and skipped on PRs, so a capture gap (missing Simulator runtime, or no Screen Recording grant for the mac window capture) never reds the core signal. Generated PNGs (clients/apple/screenshots/) are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3820aab2c4 |
fix(apple): drive DualSense rumble over raw HID (CoreHaptics is silent on macOS)
GameController's CHHapticEngine never reaches the DualSense's motors on macOS — its
adaptive triggers and lightbar work, but rumble stays silent (a documented platform
gap). Drive the motors directly via the DualSense HID output report instead, the way
SDL and the Linux hid-playstation driver do — the same report that already rumbles
the pad on a Linux host. Confirmed live on macOS.
- DualSenseHID (macOS): opens the Sony DualSense via IOHIDManager and writes the USB
(0x02, 48 bytes) and Bluetooth (0x31, 78 bytes + CRC32) output reports through
IOHIDDeviceSetReport. Allowed under the App Sandbox by the existing device.usb +
device.bluetooth entitlements; coexists with GameController (non-seized open).
Flags mirror the kernel driver (COMPATIBLE_VIBRATION | HAPTICS_SELECT +
COMPATIBLE_VIBRATION2); valid_flag1 = 0 so a rumble report leaves the
GameController-managed lightbar / triggers / player LEDs untouched.
- RumbleRenderer routes a DualSense to the HID backend and keeps CoreHaptics for
every other pad, fixing both live sessions and the test panel (shared renderer).
- CoreHaptics path reworked too: bake the target intensity + an explicit sharpness
into the continuous event (the dynamic-parameter scaling is silent on controller
engines) and tear down outside the inout access to fix a latent exclusivity hazard.
Adds a DEBUG-only Settings -> Controllers -> "Test Controller" panel (ControllerTestView
+ ControllerTester) that shows live input and fires rumble / adaptive triggers /
lightbar / player LEDs straight at the pad, with a readout of the active rumble backend
("DualSense HID - USB/Bluetooth"). Used to validate the fix.
Tests: DualSenseHIDTests pins the USB/BT report layout and the BT CRC32 (canonical
0xCBF43926 check vector). Debug + release build clean; gamepad suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
99ae1b9a17 |
feat(gamepad): add virtual Xbox One/Series + DualShock 4 pad types
Extends virtual-controller support beyond Xbox 360 + DualSense. Goal: a physical Xbox One or PS4 pad on the client gets a near-native matching virtual pad on the host, auto-resolved from the controller type. Protocol/core: - GamepadPref gains XboxOne (wire 3) + DualShock4 (wire 4); to_u8/from_u8/ from_name/as_str + C ABI PUNKTFUNK_GAMEPAD_XBOXONE/_DUALSHOCK4 constants (compile-time guard ties them to the enum). Single-byte wire form is unchanged, so it's forward-compatible (older peers degrade to Auto). Host (Linux): - New UHID DualShock 4 backend (inject/dualshock4.rs) bound by hid-playstation: lightbar, touchpad, motion, rumble — DualSense minus adaptive triggers / player LEDs / mute. Reuses the DualSense pure state + button mapping; only the report byte layout, the real-DS4 HID descriptor, the GET_REPORT handshake (0x12 MAC mandatory; 0x02 calibration; 0xa3 firmware) and the touchpad resolution (1920x942) differ. Touchpad/motion ride the existing 0xCC plane, lightbar the 0xCD Led plane (deduped); rumble the universal 0xCA plane. - Xbox One/Series is the uinput Xbox-360 backend parameterized with the One S USB identity (045e:02ea) for matching glyphs — XInput-identical otherwise. - PadBackend dispatch + resolver handle both; off Linux the UHID pads and One/Series fold into Xbox 360. Windows-host DS4 (ViGEm) deferred. Clients (auto-resolve physical pad -> virtual type, plus manual settings): - Linux/Windows (SDL3): SDL_GAMEPAD_TYPE_PS4 -> DualShock 4, _XBOXONE -> Xbox One; PadInfo carries the resolved pref; DS4 touchpad/motion capture + lightbar already type-agnostic. Linux settings combo + label updated. - Apple (GameController): GCDualShockGamepad/GCXboxGamepad detection, DS4 touchpad capture, settings picker entries. - Android (Kotlin): InputDevice VID/PID auto-detect (matching the other clients) + settings entries. - probe: --gamepad help/aliases. Also hardens the Android JNI boundary: wrap the teardown + poll-thread shims in catch_unwind so a panic degrades to a logged no-op instead of aborting the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ddd65d732 |
feat(clients): HDR Steps 2-3 — apply mastering metadata + display capability-gate
Continues docs/hdr-pipeline-plan.md. Steps 0/1 + Step 2 (Windows/Android) already landed in ffae2a3; this is Step 2 (Apple) + Step 3 (all clients). Client-only — no core/host/ABI change (the 0xCE/next_hdr_meta/color_info surfaces shipped in Step 0). Step 2 — clients APPLY the host's HDR metadata (each remaps from the wire form: ST.2086 G,B,R order, mastering luminance in 0.0001 cd/m2): - Apple: connect via punktfunk_connect_ex5 (resurrects the previously-dead HDR pipeline); nextHdrMeta/colorInfo wrappers + HdrMeta SEI-blob builders; the pump drains nextHdrMeta -> VideoDecoder.setHdrMeta -> CVBufferSetAttachment of MasteringDisplayColorVolume (24B BE) + ContentLightLevelInfo (4B BE) on each HDR pixel buffer (correct for the itur_2100_PQ layer; CAEDRMetadata avoided as ambiguous there). Step 3 — capability-gate: advertise HDR caps ONLY when the display can present it, so an SDR display gets a proper BT.709 stream instead of PQ it would mis-tone-map; an HDR display self-tone-maps from the Step-1/2 mastering metadata. - Windows: present::display_supports_hdr() (DXGI any IDXGIOutput6 colour space == G2084), ANDed with the user HDR setting in session.rs; logs the SDR drop. - Apple: NSScreen.maximumExtendedDynamicRangeColorComponentValue>1 (macOS) / UIScreen.main.potentialEDRHeadroom>1 (iOS) in SessionModel. - Android: Settings.displaySupportsHdr (Display.getHdrCapabilities HDR10/HDR10+) passed through a new hdr_enabled jboolean on nativeConnect; session.rs gates the caps. Validation: Android native (incl. the jboolean gate) builds + clippy clean via cargo-ndk; fmt clean. Windows (MSVC), Apple (Swift) and the Kotlin side are CI/on-glass validated — not compilable on the Linux dev box. Deferred to the RTX box: mid-session Reconfigure SDR-downgrade on monitor move, and confirming the host emits SDR for an SDR client off an HDR desktop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f48c739a9c |
fix build
improve iOS & iPadOS UI |
||
|
|
f805e54cbc | fix - replace Punktfunkempfänger with Punktfunk | ||
|
|
3cc0c65c11 |
fix(apple): fill the notch in macOS fullscreen — stop letterboxing below the camera housing
The macOS sessionView branch was missing the .ignoresSafeArea() its iOS/tvOS siblings have, so in fullscreen the stream was laid out in the safe area below the notch; the aspect-fit video then scaled down to that smaller area and left black borders. Add .ignoresSafeArea() so the stream fills the whole display including behind the camera housing (a thin top-center strip occluded — normal fullscreen- video behavior); at the display's native mode it's now a 1:1 fill. Inert in windowed mode and on non-notched displays. NSPrefersDisplaySafeAreaCompatibilityMode is deliberately not used (it shrinks the whole window with borders on all sides). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2b01294eb7 |
feat(apple): tabbed macOS Settings + stats-overlay placement/toggle + Stream menu
The macOS Settings window had outgrown one scrolling pane — split it into a tabbed preferences window (General / Display / Audio / Controllers / Advanced). Each settings group is now a shared @ViewBuilder section, so iOS keeps its single grouped Form and tvOS its pushed-picker layout, each defined once. No setting moved or dropped. New statistics-overlay controls (Settings → Display → Statistics): a show/hide toggle (DefaultsKey.hudEnabled) and a corner picker (HUDPlacement / DefaultsKey.hudPlacement) — the HUD moves to the chosen corner and aligns its text to that edge. A Scene-level "Stream" menu (StreamCommands) carries Show/Hide Statistics (⌘⇧S) and Disconnect (⌘D). Disconnect moved off the HUD button into the menu so it survives the overlay being hidden, wired via .focusedSceneValue. On iOS a material-backed exit chip appears when the HUD is hidden (touch users have no menu/⌘D); tvOS disconnect is unchanged (Siri-Remote Menu button). Builds on macOS/iOS/tvOS; swift test green. Adversarially reviewed (8 findings refuted, 2 minor — the iOS exit-chip contrast fix is included here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
159f6bff5b |
feat(trust): host-gated trust-on-first-use — PIN pairing mandatory by default
TOFU let anyone who could reach the host click "Trust" and stream, which defeats the point on a LAN. Make SPAKE2 PIN pairing the default and only way to trust a NEW host; TOFU survives as an explicit HOST opt-in (for fully trusted networks), advertised over mDNS so clients render their trust UI from the host's policy rather than offering trust on faith. Contract: - Host advertises pair=required (default) or pair=optional. pair=required rejects unpaired clients at the handshake; pair=optional accepts them (TOFU). - Clients: a pinned host whose fingerprint matches connects silently; a pinned host whose fingerprint CHANGED forces re-pairing via PIN (no re-trust shortcut); a NEW host is offered TOFU only if it advertised pair=optional, otherwise PIN pairing is mandatory; a manually-typed or unknown-policy host is always PIN. Host (crates/punktfunk-host/src/main.rs): - m3-host now REQUIRES pairing by default (was open by default). New --allow-tofu opts into accepting unpaired clients + advertising pair=optional; pairing is always armed (PIN logged at startup). serve --native was already secure-by-default (serve --open). The mDNS advert and the accept loop already mapped require_pairing -> pair=required + reject; only the m3-host CLI default + help text changed. Clients honor the advertised policy: - Android (MainActivity.kt): TOFU only for a discovered pair=optional host; manual/unknown -> PIN; fp-change -> re-pair only (dropped the "Forget & re-TOFU" shortcut). - Apple (HostDiscovery/SessionModel/ContentView/HostCards/HostStore): new allowsTofu (pair==optional, distinct from unknown); connect() gates .awaitingTrust on it; unpinned non-optional hosts route to the PIN sheet; "Forget Identity" re-pairs rather than re-TOFUs. - Linux (app.rs/ui_hosts.rs/session.rs): ConnectRequest.pair_required -> pair_optional; initiate_connect routes pinned/fp-changed/optional/else; manual + --connect unknown -> PIN; a pinned connect rejected on trust grounds re-pairs. Docs (CLAUDE.md, README.md, docs-site/content/docs/pairing.md): describe the gated model — PIN is the default, TOFU an explicit opt-in with an impostor warning. Verified: host cargo check/clippy/fmt clean; Android built + live (emulator -> home-worker-2): a manual connect now opens the PIN dialog (no Trust button) and the PIN ceremony streams; Apple swift build clean; Linux clippy -D warnings + fmt clean on the Linux box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
609528e89f |
feat(apple/library): mTLS — authenticate by the paired identity, drop the token
Phase 3: the Apple library now talks to the host's HTTPS mgmt API (
|
||
|
|
0c8bee8d1f |
fix(apple/cursor): disable the client-side cursor (gamescope traps input)
The client-side cursor positions the host pointer with ABSOLUTE events, but gamescope's input socket (EIS) grants only a relative pointer — the host drops the absolute events (libei.rs: no PointerAbsolute → not emitted), so the pointer never moves and clicks/scroll land on the stuck position. Auto-mode enabled exactly this on gamescope, making all input appear dead until toggled off. Force `cursorVisible = false`, neuter the ⌘⇧C toggle, and hide the now-inert Settings picker. The resolution logic + handlers are kept (commented) for when per-compositor gating (KWin/GNOME/Sway have an absolute pointer) or a synthetic-cursor-over-relative path lands. Relative capture (the working path) is now always used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5b5749c6d2 |
fix(apple): capture the PS/Home button + fullscreen only while streaming
Two issues from live Mac testing, plus a requested fullscreen option: - PS button: the Home/PS button (→ guide; the host maps it to the DualSense PS bit) does not reliably fire GCExtendedGamepad.valueChangedHandler on macOS, so its presses were dropped. Add a dedicated buttonHome.pressedChangedHandler that re-syncs. The host already maps BTN_GUIDE→PS, so this is the missing client half. - Fullscreen: a macOS FullscreenController (NSViewRepresentable) takes the window fullscreen while a session is up (incl. the trust prompt over the blurred stream) and restores it on the host list — so only the stream is fullscreen, not the picker. New `fullscreenWhileStreaming` setting (default on) + a Settings "Window" toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
024e255476 |
feat(apple/library): launch a picked title (step 4 client side)
Tapping a game in the (flagged) library now starts a session that asks the host to launch it — the picked GameEntry id rides the connect down to the host, which resolves it against its own library ( |
||
|
|
2ba61e5257 |
feat(apple/library): experimental game-library browser (flagged off)
Plan step 3 — the Apple client surfaces the host's game library, behind a feature flag (`DefaultsKey.libraryEnabled`, default OFF). Browsing only; launching a chosen title is step 4. - PunktfunkKit `LibraryClient`: Codable GameEntry/Artwork/LaunchSpec mirroring crates/punktfunk-host/src/library.rs, and an async fetch of GET /api/v1/library with a bearer token. Typed LibraryError guides setup (the common case is "needs a --mgmt-token"). `Artwork.posterCandidates` = portrait → header → hero. - `LibraryView`: cross-platform poster grid (LazyVGrid, AsyncImage that walks the art candidates past load failures to a text placeholder), a store badge, and an inline Connection form (mgmt port + token) that surfaces when the API is unreachable / 401 / no token set. Read-only. - StoredHost gains `mgmtPort`/`mgmtToken` (the mgmt API is a distinct port from the data plane and needs a token off-loopback). Both OPTIONAL — synthesized Decodable ignores property defaults but treats a missing Optional as nil, so older saved hosts decode unchanged (a defaulted non-optional would wipe the list). HostStore.setMgmt. - Entry point: a flag-gated "Browse Library…" host-card context action → LibraryView (sheet on macOS/iOS, pushed on tvOS), mirroring the pair/speed-test plumbing. Plus a Settings "Experimental" toggle. Can't compile Swift on the Linux dev box; CI (apple.yml: swift build + swift test on the mac mini) verifies the macOS path. Added LibraryClientTests (decode + art order) for `swift test`. iOS/tvOS-only branches mirror existing patterns. Live-verify on the Mac pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c47462e7ca |
feat(apple): client-side cursor for gamescope sessions (toggle + shortcut)
gamescope's PipeWire capture carries no cursor (verified upstream — it never
composites the cursor or adds SPA_META_Cursor), so the cursor must be drawn on the
client. New macOS "cursor-visible" capture mode: instead of disassociating+hiding
the system cursor and sending relative deltas (the game path, unchanged), it keeps
the system cursor visible over the stream and sends ABSOLUTE positions
(MouseMoveAbs), mapped through the video's aspect-fit (AVMakeRect) to host pixels
with the letterbox bars dropped. The visible system cursor IS the client cursor —
zero added latency, no double cursor (gamescope draws none), accurate (the client
drives the host's absolute mouse).
- Default: on iff the session's resolved compositor is gamescope (via the new
punktfunk_connection_compositor getter,
|
||
|
|
3eaf218a47 |
feat(net/mac): default-on recvmsg_x batched Mac recv + GSO host + longer probe
The Mac/iOS client's wall around ~380 Mbps on a 2.5 G path is the receive drain, not the transport: a loopback speed-test pushes 380/600/1000 Mbps at 0.0% loss, but Darwin has no recvmmsg(2), so the macOS client was doing one recv() syscall per packet — ~40-90k syscalls/s on one core. When the recv loop can't drain fast enough the kernel socket buffer backs up and drops, which the client sees as a sustained stream stalling/freezing in the 300-400 Mbps range (and an immediate "session ended" when a 500 Mbps+ first keyframe bursts in). - core/transport: flip recvmsg_x (the batched Darwin recv, ~30x fewer syscalls) from opt-in to default ON, opt-out via PUNKTFUNK_RECVMSG_X=0. Keeps the auto-fallback to the scalar loop on any unexpected syscall error. The Apple CI swift-test loopback now exercises this path by default. - packaging/kde host.env: enable PUNKTFUNK_GSO=1 — UDP segmentation offload on the host send path (one sendmsg per ~64 packets), the dominant lever above ~1 Gbps. Already wired (send_sealed -> send_gso) with sendmmsg auto-fallback. - apple SpeedTestSheet: lengthen the bandwidth probe 2 s -> 5 s so the measured number stops swinging wildly (50 vs 900 Mbps on the same link) — long enough for steady-state send + recv drain to settle. Matches host MAX_PROBE_MS. - host capture: PUNKTFUNK_SYNTH_NOISE synthetic high-entropy source for reproducible throughput testing of the encode->FEC->send->recv path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
be91622ae7 |
feat(apple): surface host online status on the home grid
Saved host cards now show a presence dot — green when the host is advertising on
the LAN right now, grey when not seen. Cross-references each StoredHost against the
live mDNS discovery set (HostDiscovery). No host changes: the host already
advertises _punktfunk._udp with a stable id + cert fingerprint, which the client
already browses.
- StoredHost.matches(DiscoveredHost): fingerprint-first (survives a DHCP address
change), address:port fallback. The discovered-section dedup now uses the same
match, so a saved host whose IP changed no longer also shows up as a stranger.
- HostCardView gains an isOnline presence dot (accessibility-labelled).
- HomeView.isOnline recomputes on every @Published discovery change, so the dot
tracks hosts joining/leaving the network live.
Online detection is LAN-scoped by design: a remote/cross-subnet host that doesn't
advertise here shows grey ("not seen"), not a false "offline". Swift-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a5a49184c0 |
fix(apple): persist Keychain trust — sign macOS + data-protection keychain
The client identity prompted for Keychain access on every launch/rebuild. Root cause: the macOS app target was ad-hoc signed (CODE_SIGN_IDENTITY = "-"), and the identity lived in the file keychain whose "Always Allow" ACL is bound to the app's exact code signature (cdhash for ad-hoc). Every rebuild changed the binary -> changed the cdhash -> the ACL no longer matched -> re-prompt. - Sign the macOS target with Apple Development (team already set) instead of ad-hoc, so the designated requirement is identity-based and stable across rebuilds. - Move the identity to the data-protection keychain (kSecUseDataProtectionKeychain) gated by a team-scoped keychain-access-group entitlement — access is granted by the app's entitlement, not a per-binary ACL, so it's prompt-free and survives rebuilds. Add Config/Punktfunk.entitlements and wire CODE_SIGN_ENTITLEMENTS into all six app configs (macOS/iOS/tvOS). - Unsigned / ad-hoc builds (e.g. `swift run`) lack the entitlement (errSecMissingEntitlement) — fall back to the legacy file keychain so they still work (with the old prompt), no hard failure. macOS re-mints the identity on first run (the old file-keychain copy isn't in the data-protection keychain) -> one re-pair, which is acceptable. iOS keeps its identity (the explicit access group equals the prior default). Validated: swift build; swift test (39 passed, 0 failures); xcodebuild -showBuildSettings confirms Apple Development + Config/Punktfunk.entitlements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
087b077980 |
refactor(apple): decompose ContentView (735 -> 272 lines)
Split the monolithic ContentView into focused view files — a pure structural refactor with no behavior change (verified: builds macOS/iOS/tvOS, the test suite is green, and a fidelity review against the original found no discrepancies): - ContentView (272): the coordinator — owns the session model / host store / discovery, switches home<->session, holds the connect logic (it reads @AppStorage) + the dev hooks, and the stream builder (whose stable identity across awaiting-trust->streaming must NOT move — it stays here). - HomeView (251): the hosts grid + navigation + toolbar + sheets + "On this network" discovery section + empty state. - HostCards (158): HostCardView + DiscoveredCardView, sharing a CardMetrics struct (dedupes the platform-tuned sizing the two cards had copy-pasted). - TrustCardView (80): the TOFU prompt + fingerprint formatting. - StreamHUDView (67): the streaming overlay HUD. State flows idiomatically: @StateObject (ContentView) -> @ObservedObject in subviews, @State -> @Binding; the connect logic is passed as closures. Sheet placement is preserved — the pairing/speed-test sheets stay on the outer body so they survive the trust->home transition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2be7f715bf |
refactor(apple): code-quality pass — audit fixes + centralized defaults keys
A 6-agent adversarial audit of the client (11 confirmed of 39 findings, the rest
filtered) drove these:
- fix: SessionAudio ring buffer — guard a write larger than the ring (would push
readIdx past writeIdx and corrupt the buffer; never happens, but guard not corrupt).
- fix: CADisplayLink retain cycle (stage-2 presenter) — a weak-target DisplayLinkProxy
so the view can deallocate (the link retains its target); stage-2 teardown added to
both StreamView/StreamViewController deinits as a safety net.
- fix: GamepadFeedback deinit { flag.stop() } — the drain thread holds the connection
strongly and self weakly, so an abrupt teardown without stop() would leak it.
- refactor: centralize the 12 UserDefaults/@AppStorage key literals (scattered across
8 files) into one DefaultsKey enum — a typo silently splits a setting's reader from
its writer.
- docs: RumbleRenderer @unchecked Sendable invariant; the HID digit-row table; the
stage-2 layer compositing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
cde9004e8a |
feat(apple): stage-2 presenter — explicit decode + Metal present + glass-to-glass
Opt-in (Settings -> Presenter; `punktfunk.presenter`, default stage-1). Stage-1's AVSampleBufferDisplayLayer decodes AND presents internally with no per-frame callback, so neither decode nor present can be stamped or hand-paced. Stage-2 takes explicit control: - VideoDecoder: VTDecompressionSession, async output callback stamps decode-completion, session rebuilt on every IDR / format change. Unit-tested (testVideoDecoderAsyncCallbackDeliversPixels). - MetalVideoPresenter: CAMetalLayer + CVMetalTextureCache + a runtime-compiled BT.709 limited-range NV12->RGB shader, present at the next vsync. The CVMetalTextures + pixel buffer are held until the GPU completes. - Stage2Pipeline: pump thread -> decoder -> newest-ready 1-slot ring; the hosting view's display link drains it once per vsync and stamps capture->present (the display-link target time projected into CLOCK_REALTIME). - LatencyMeter gains record(ptsNs:atNs:offsetNs:); the HUD shows a capture->present (glass-to-glass, modulo host render->capture) line, skew-corrected via clockOffsetNs. Measured live ~11 ms p50 vs ~2.2 ms capture->client. - StreamView / StreamViewIOS host the CAMetalLayer as a sublayer + a CADisplayLink (NSView.displayLink on macOS) when stage-2; input capture + HUD unchanged. The session-active gates switch from `pump != nil` to `connection != nil` so capture engages without a StreamPump. Validated: builds macOS/iOS/tvOS; the decode half is unit-tested; the Metal present is live-validated on glass (correct image + the capture->present number). Colorspace is BT.709 SDR for now; 10-bit/HDR + a pacing policy are later. Plan: docs-site/content/docs/apple-stage2-presenter.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
17b4a5edf3 |
fix(apple): latency HUD — interpolate the (same-host) suffix, don't concat
The capture->client latency line concatenated a String onto a LocalizedStringKey
(Text("...\(x, specifier:)..." + (cond ? "" : "...")), which doesn't type-check:
the specifier: interpolation makes the literal a LocalizedStringKey, which has no
'+'. Fold the conditional suffix into the interpolation instead — the Apple
client didn't build on the latency-HUD commit (
|