Commit Graph

115 Commits

Author SHA1 Message Date
enricobuehler af9bb54785 feat(android): D-pad / game-controller focus navigation (TV + phone)
apple / swift (push) Successful in 54s
windows-host / package (push) Failing after 2m14s
android / android (push) Has been cancelled
ci / web (push) Successful in 29s
ci / docs-site (push) Failing after 17s
ci / rust (push) Successful in 4m35s
ci / bench (push) Failing after 4m33s
decky / build-publish (push) Successful in 13s
deb / build-publish (push) Successful in 3m10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 32s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 7m11s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m12s
docker / deploy-docs (push) Successful in 9s
Make a controller drive the Compose UI when not streaming, so the menus work on a TV
remote AND on a controller paired to a phone:
- MainActivity maps gamepad face buttons to the keys Compose's focus system
  understands (A -> DPAD_CENTER to activate, B -> BACK); D-pad *keys* already move
  focus and pass through untouched.
- For controllers whose D-pad reports as HAT axes (or to navigate with the left
  stick), dispatchGenericMotionEvent converts AXIS_HAT_X/Y / AXIS_X/Y into discrete
  D-pad key events, edge-detected so a held direction moves focus exactly once.
- HostCard draws a clear primary-colour focus border (the default state layer is too
  subtle across a room on TV).

All gated on "not streaming" -- during a stream the controller still forwards to the
host unchanged. Compile-verified (./gradlew :app:assembleDebug); the focus behaviour
itself needs on-device validation (no KVM here for a TV emulator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:14:29 +00:00
enricobuehler f4cff765ed fix(decky): scrub PyInstaller LD_LIBRARY_PATH before spawning system flatpak
apple / swift (push) Successful in 53s
android / android (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
decky / 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
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 Loader is a PyInstaller binary; it puts its bundled (older) libssl/libcrypto
on LD_LIBRARY_PATH via its /tmp/_MEI* unpack dir, and that env leaked into the
backend's `flatpak run`/`flatpak kill` subprocess. The SYSTEM flatpak's libcurl
+ libostree need newer OPENSSL symbols (3.2/3.3/3.4), so pairing failed with
"libssl.so.3: version OPENSSL_3.3.0 not found". _flatpak_env() now restores
each LD_*_ORIG PyInstaller saved, or drops the var, so the system loader uses
system libs. Reproduced + verified on the Deck (SteamOS 3.8.10, Flatpak 1.16.6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:03:39 +00:00
enricobuehler b9e50faa40 polish(android): grouped Settings cards + ConnectScreen error banner & search indicator
apple / swift (push) Successful in 54s
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 (--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
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
android / android (push) Has been cancelled
- Settings: flat list -> Display / Host / Audio / Overlay sections in outlined
  cards (SettingsGroup + ToggleRow helpers) with section headers.
- ConnectScreen: connection errors now show in a filled errorContainer banner
  (was plain red text lost in the layout), and a "Searching the local network..."
  spinner appears while discovery is active but nothing's turned up yet.

Verified locally: ./gradlew :app:assembleDebug BUILD SUCCESSFUL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:58:35 +00:00
enricobuehler f39230e8f4 fix(android): crash on back-while-streaming (UAF) + Material You theme & connect polish
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m37s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 35s
android / android (push) Successful in 4m23s
deb / build-publish (push) Successful in 2m37s
decky / build-publish (push) Successful in 24s
ci / bench (push) Successful in 4m27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m45s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 3m21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 23s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m28s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m30s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m52s
Crash: DisposableEffect.onDispose called nativeClose(handle) (Box::from_raw frees
the SessionHandle) while the SurfaceView's surfaceDestroyed independently called
nativeStopVideo/Audio/Mic on the same handle -- whichever ran after the close
dereferenced freed memory (SIGSEGV: the consistent back-navigation crash). Add a
one-shot `closed` guard: onDispose marks it before freeing; surfaceDestroyed skips
the native calls once closed (backgrounding still stops the threads when it wins).

Polish:
- Branded Material You theme (Theme.kt): dynamic colour on Android 12+, punktfunk
  brand violets as the pre-12 fallback, replacing the generic darkColorScheme().
- ConnectScreen: "Connecting..." was rendered in error-red with no spinner; now a
  neutral spinner while connecting, red reserved for actual errors.

Verified locally: ./gradlew :app:assembleDebug BUILD SUCCESSFUL (both ABIs + the
Compose changes), debug APK assembles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:49:51 +00:00
enricobuehler 55cd58e487 fix(android): DataSpace impls Display not Debug — use {ds} in HDR logs
apple / swift (push) Successful in 54s
deb / build-publish (push) Has been cancelled
decky / build-publish (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
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
android / android (push) Successful in 4m27s
ci / rust (push) Successful in 2m14s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m38s
ndk's DataSpace derives Copy/PartialEq/Eq and impls Display (no Debug), so the
{ds:?} in the HDR dataspace log statements wouldn't compile under cargo-ndk.
Host clippy can't catch it — decode.rs is android-gated. Switch to {ds}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:13:20 +00:00
enricobuehler 1cd5e0e375 feat(android): HDR (Main10 / BT.2020 PQ) + fix ndk feature gating
apple / swift (push) Successful in 54s
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
deb / 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
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
Mirrors the Apple client's HDR path so the Android client can display HDR from a
Windows HDR host:
- nativeConnect now advertises VIDEO_CAP_10BIT | VIDEO_CAP_HDR (was 0), so the
  host upgrades to a Main10 / BT.2020 PQ encode.
- decode.rs detects HDR reactively from the decoder's reported output colour
  (color-transfer ST2084=6 / HLG=7, color-range) -- the AMediaCodec analogue of
  VideoToolbox's format description on Apple -- and signals the Surface dataspace
  (Bt2020[Itu]Pq / Bt2020[Itu]Hlg) so the compositor/display switch to HDR.
  AMediaCodec decodes Main10 from the in-band SPS; no profile override needed.

Also fixes the Android build: set_frame_rate (added in 5262e28) is gated on the
ndk `nativewindow` + `api-level-30` features, which weren't enabled -- so that
commit could not compile under cargo-ndk. Enable
features = ["media","audio","nativewindow","api-level-31"] (minSdk 31): covers
set_frame_rate (api-30), set_buffers_data_space + the DataSpace module (api-28),
and ANativeWindow (nativewindow).

Verified host-side: fmt --all + clippy --workspace (the caps advertise + JNI
surface). The android-gated decode + NDK gating verified against the ndk 0.9
sources; android.yml (cargo-ndk) is the compile gate, and real HDR display needs
an HDR device + Windows HDR host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:09:54 +00:00
enricobuehler 5262e28b79 feat(android): live stats HUD + low-latency decode tuning
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m1s
decky / build-publish (push) Has been cancelled
deb / build-publish (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
windows / build (push) Successful in 55s
audit / cargo-audit (push) Failing after 1m8s
android / android (push) Failing after 2m12s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m31s
ci / rust (push) Successful in 6m31s
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 35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m44s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m25s
flatpak / build-publish (push) Successful in 5m5s
docker / deploy-docs (push) Successful in 20s
Stats HUD (mirrors the Apple client): the decode thread accumulates FPS, receive
throughput, and capture->client latency (p50/p95, skew-corrected) in Rust
(clients/android/native/src/stats.rs); nativeVideoStats drains a snapshot ~1 Hz
over JNI as a DoubleArray. StreamScreen renders a Compose overlay
(W*H@Hz / fps / Mb/s / latency, + dropped-under-loss), toggled by a Settings
switch (persisted, default on) or a 3-finger tap.

Performance (decode.rs):
- ANativeWindow_setFrameRate(refresh_hz): align display vsync to the stream rate
  (no 60-in-120 judder); safe since minSdk 31 >= API 30.
- Raise the decode thread toward URGENT_DISPLAY (best-effort setpriority) so
  background work can't preempt it under load.
- Codec low-latency hints KEY_PRIORITY=0 (realtime) + KEY_OPERATING_RATE.

Verified host-side: cargo build/clippy/fmt --workspace (the ungated stats + JNI
accessor). The android-gated decode.rs (NDK) and the Kotlin build only in CI
(android.yml: gradle + cargo-ndk) -- APIs verified against crate sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:49:29 +00:00
enricobuehler 7121b0eb43 fix(apple): disarm CHHapticEngine handlers with no-ops, not nil
apple / swift (push) Successful in 55s
android / android (push) Failing after 1m58s
ci / rust (push) Failing after 1m13s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 30s
decky / build-publish (push) Successful in 10s
deb / build-publish (push) Successful in 2m27s
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 4s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m31s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
docker / deploy-docs (push) Successful in 24s
stoppedHandler/resetHandler are non-optional closures on the CI SDK
((StoppedReason)->() and ()->()), so assigning nil fails to compile
(apple.yml). Assign no-op closures to disarm them before engine.stop()
-- same re-entrancy guard intent, type-correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:20:37 +00:00
enricobuehler 9c8fa9340c refactor: drop milestone names + consolidate clients; loss-recovery & rumble fixes
apple / swift (push) Failing after 40s
audit / cargo-audit (push) Failing after 1m12s
windows-msix / package (push) Successful in 1m37s
windows / build (push) Successful in 1m14s
android / android (push) Successful in 4m48s
ci / web (push) Successful in 27s
ci / rust (push) Successful in 4m21s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 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 19s
deb / build-publish (push) Successful in 6m3s
flatpak / build-publish (push) Successful in 4m13s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m16s
docker / deploy-docs (push) Successful in 18s
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>
2026-06-18 21:05:58 +00:00
enricobuehler 1faa6c6ad4 ci(android): replace r0adkll with a direct Play Publishing-API upload
ci / rust (push) Successful in 1m39s
ci / web (push) Successful in 32s
deb / build-publish (push) Successful in 2m31s
decky / build-publish (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
apple / swift (push) Successful in 53s
ci / docs-site (push) Successful in 31s
android / android (push) Successful in 4m6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 4m43s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m18s
r0adkll/upload-google-play hides real API errors behind "Unknown error
occurred." Proved the full upload sequence (insert edit -> upload bundle ->
track update -> validate) succeeds with the service account, so the failure was
r0adkll's opaque error handling and/or a base64-encoded SERVICE_ACCOUNT_JSON
secret.

clients/android/ci/play-upload.py does the same sequence with stdlib + openssl
(no pip), reuses the SERVICE_ACCOUNT_JSON secret, tolerates it being raw JSON or
base64, auto-retries commit with changesNotSentForReview, and prints Google's
actual error. Locally dry-run-validated against the live app (both secret forms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:38:20 +00:00
enricobuehler 9abb9a2496 fix - replace Punktfunkempfänger with Punktfunk
apple / swift (push) Successful in 55s
ci / rust (push) Successful in 1m38s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 30s
deb / build-publish (push) Successful in 2m36s
decky / build-publish (push) Successful in 23s
ci / bench (push) Successful in 4m36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m57s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m26s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m16s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m19s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m1s
android / android (push) Failing after 3m27s
2026-06-18 17:56:58 +02:00
enricobuehler 02b1be652d cancel rumble on disconnect
apple / swift (push) Successful in 56s
ci / rust (push) Successful in 1m37s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 33s
android / android (push) Failing after 3m55s
deb / build-publish (push) Successful in 2m27s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m40s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m36s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m28s
docker / deploy-docs (push) Successful in 19s
hide system bar in StreamScreen.kt
2026-06-18 17:20:57 +02:00
enricobuehler b8c9f88cfd feat: add .env support for local release builds
apple / swift (push) Successful in 53s
ci / rust (push) Successful in 1m38s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 37s
android / android (push) Failing after 4m20s
deb / build-publish (push) Successful in 2m35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 12s
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 4s
ci / bench (push) Successful in 4m44s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m24s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m30s
docker / deploy-docs (push) Successful in 18s
2026-06-18 12:40:51 +02:00
enricobuehler 8f720e0e46 chore: bump version to 0.0.2 to trigger Play Store CI
android / android (push) Failing after 43s
apple / swift (push) Successful in 54s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 28s
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
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
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
ci / rust (push) Successful in 1m36s
ci / bench (push) Has been cancelled
2026-06-18 12:22:22 +02:00
enricobuehler a24679ce69 feat: setup CI for Google Play Store submission and refactor UI
android / android (push) Failing after 50s
apple / swift (push) Successful in 54s
deb / build-publish (push) Successful in 2m25s
ci / web (push) Successful in 28s
ci / rust (push) Successful in 1m36s
ci / docs-site (push) Successful in 28s
decky / build-publish (push) Successful in 11s
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 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m25s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m4s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m55s
2026-06-18 11:51:40 +02:00
enricobuehler 6c02acab59 build: update Android NDK to r30 (30.0.14904198)
android / android (push) Failing after 42s
apple / swift (push) Successful in 57s
ci / web (push) Successful in 32s
ci / rust (push) Successful in 1m43s
ci / docs-site (push) Successful in 29s
deb / build-publish (push) Successful in 2m26s
decky / build-publish (push) Successful in 23s
ci / bench (push) Successful in 4m33s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3m18s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m25s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m35s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m29s
docker / deploy-docs (push) Successful in 23s
2026-06-18 11:13:14 +02:00
enricobuehler 15d3d423fa feat(decky): full-featured Gaming-Mode client — fullscreen page, pairing, focus-correct launch
apple / swift (push) Successful in 56s
ci / docs-site (push) Successful in 28s
ci / rust (push) Successful in 1m48s
android / android (push) Successful in 2m11s
ci / web (push) Successful in 27s
deb / build-publish (push) Successful in 2m24s
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 7s
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 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / bench (push) Successful in 4m32s
flatpak / build-publish (push) Successful in 4m1s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m18s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m43s
The plugin was a QAM launcher whose stream never appeared, with no
pairing. Three fixes, plus a headless --pair mode on the GTK client:

- Stream actually starts (MoonDeck's proven mechanism): gamescope only
  focuses the process tree Steam launched via reaper, so a flatpak
  spawned from the (root) backend is invisible. The frontend now
  registers ONE hidden non-Steam shortcut pointing at bin/punktfunkrun.sh,
  passes the host as the shortcut's Steam launch options, and starts it
  with SteamClient.Apps.RunGame — gamescope then fullscreen-focuses it.
  The wrapper execs `flatpak run io.unom.Punktfunk --connect <host>`.
- Fullscreen page: routerHook.addRoute("/punktfunk") — host list,
  per-host Pair/Stream, and a settings section (resolution/refresh/
  bitrate/gamepad/mic, written to client-gtk-settings.json).
- Pairing: a gamepad-navigable PIN keypad. The host shows the PIN; the
  backend runs the SPAKE2 ceremony headlessly via the client's new
  `--pair <PIN> --connect host` CLI mode (app.rs), persisting the host
  as paired so the stream then connects silently. Same flatpak =>
  shared identity store, verified live (ceremony against a real host).
- Backend (main.py): discover / pair / runner_info / get_settings /
  set_settings / kill_stream; uses DECKY_USER_HOME so paths resolve to
  the deck user's flatpak install regardless of the plugin's root flag.

CI (decky.yml) and the sideload packager now ship bin/punktfunkrun.sh.
The Steam-shortcut launch and headless-pairing env follow MoonDeck
exactly but need a Deck in Gaming Mode to fully confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:17:14 +00:00
enricobuehler 1bcb786382 fix(android): request NEARBY_WIFI_DEVICES at runtime so mDNS discovery works on real devices
apple / swift (push) Successful in 53s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 38s
android / android (push) Successful in 3m23s
deb / build-publish (push) Successful in 2m4s
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 5s
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 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 4s
ci / bench (push) Successful in 4m31s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m8s
docker / deploy-docs (push) Successful in 5s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m58s
ci / rust (push) Successful in 4m4s
NsdManager service discovery needs NEARBY_WIFI_DEVICES on Android 13+. The app DECLARED it but
never REQUESTED it, so on a real device the permission stayed denied and discoverServices silently
found nothing — no prompt, no hosts. (It only worked on the emulator because the permission was
granted via `adb pm grant`.) Request it (mirroring the mic RECORD_AUDIO flow) when the connect
screen appears, and start/restart discovery once granted; on API < 33 discovery starts immediately
(the permission doesn't apply there). The advertised hosts the Apple clients already see will then
appear here too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:14:57 +02:00
enricobuehler 0c1afeefea fix(android): shrink the colored launcher-icon foreground to match the themed layer
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 40s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 28s
android / android (push) Successful in 5m41s
ci / bench (push) Successful in 4m28s
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 5s
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 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
deb / build-publish (push) Successful in 2m8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m27s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m38s
docker / deploy-docs (push) Successful in 18s
On the test phone's launcher the standard (colored) adaptive foreground rendered noticeably larger
than the themed (monochrome) layer — identical geometry, but the launcher insets/scales the two
differently — so the colored circles overflowed the circle mask. Shrink only the foreground group
(scale 0.105 → 0.073, re-centred) to match the correctly-sized monochrome; the monochrome layer is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:08:26 +02:00
enricobuehler 18ec32d21e feat(android): adaptive launcher icon with Material You themed-icon support
apple / swift (push) Successful in 53s
ci / rust (push) Failing after 1m36s
ci / web (push) Successful in 36s
ci / docs-site (push) Successful in 36s
deb / build-publish (push) Successful in 2m4s
decky / build-publish (push) Successful in 10s
android / android (push) Successful in 3m14s
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 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
ci / bench (push) Successful in 4m46s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m46s
docker / deploy-docs (push) Successful in 19s
Replace the placeholder system icon with the Punktfunk brand mark (two overlapping violet circles,
from the shared logo in clients/apple/.../punktfunk_Logo.icon).

- drawable/ic_launcher_foreground.xml: the violet logo (3 exact paths) scaled + centered into the
  108dp adaptive-icon safe zone via a group transform.
- drawable/ic_launcher_monochrome.xml: single-tone silhouette for Android 13+ themed icons
  (Material You) — the launcher recolors it to the wallpaper.
- mipmap-anydpi-v26/ic_launcher{,_round}.xml: adaptive-icon (background + foreground + monochrome);
  dark-indigo background (@color/ic_launcher_background) so the violet pops.
- Manifest: android:icon=@mipmap/ic_launcher + roundIcon (was @android:drawable/sym_def_app_icon).

minSdk 31 → anydpi-v26 covers every device (no legacy PNG mipmaps needed). Verified on a physical
phone (Android 16): the icon renders centered + circle-masked; the themed-icon layer is wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:02:49 +02:00
enricobuehler ef30afcf0b fix(apple): fill the notch in macOS fullscreen — stop letterboxing below the camera housing
apple / swift (push) Successful in 55s
ci / rust (push) Successful in 1m11s
android / android (push) Successful in 1m55s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 32s
ci / bench (push) Successful in 1m48s
decky / build-publish (push) Successful in 23s
deb / build-publish (push) Successful in 2m21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m37s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m56s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m42s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m51s
docker / deploy-docs (push) Successful in 21s
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>
2026-06-15 23:57:06 +02:00
enricobuehler 4b0b775e8e fix(apple): allow CoreHaptics audioanalyticsd mach-lookup under the macOS sandbox
GCDeviceHaptics.createEngine returns a CHHapticEngine (the only controller-rumble
API on Apple platforms); starting it spins up CoreHaptics, which looks up the
system audio-analytics daemon over Mach. The App Sandbox denies that global-name
lookup and the framework's precondition turns the denial into a hard crash
("Process is sandboxed but com.apple.security.exception.mach-lookup.global-name
doesn't contain com.apple.audioanalyticsd") the moment a controller's rumble
engine starts.

Add the documented, App-Store-acceptable temporary-exception whitelisting exactly
that one service. Verified embedded into the signed binary (codesign -d
--entitlements) alongside the existing entitlements. macOS-only (iOS/tvOS reject
temporary-exception keys and don't need it). App Store: declare it in App Sandbox
Entitlement Usage Information.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:57:06 +02:00
enricobuehler e99a1aea43 fix(apple): resolve QoS priority inversions + two Swift concurrency warnings
apple / swift (push) Successful in 55s
ci / rust (push) Successful in 1m31s
android / android (push) Successful in 1m48s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 33s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
ci / bench (push) Successful in 1m35s
decky / build-publish (push) Successful in 11s
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 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 3s
deb / build-publish (push) Successful in 2m19s
flatpak / build-publish (push) Successful in 4m2s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m22s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m39s
Priority inversions (Thread Performance Checker): the Apple client drains every
plane on .userInteractive threads (video pump, audio, gamepad feedback) and
connects on a .userInitiated Task, but the connector's producer threads ran at
the default QoS — so a high-QoS consumer parked waiting on a lower-QoS producer.
Pin the connector's producers (outer worker thread, all tokio runtime threads via
on_thread_start, and the data-plane spawn_blocking pump) to .userInteractive on
Apple so they match the consumers. #[cfg(target_vendor = "apple")] helper using
the existing libc dep; no-op off Apple, no Swift-side change (no latency
regression).

GamepadFeedback.swift: the init's MainActor hop captured self implicitly-strong
while the inner $active sink captured it weakly — capture [weak self] in the hop
too (the sink stays weak to avoid the retain cycle).

StreamPump.swift: the @Sendable pump-thread closure captured the non-Sendable
AVSampleBufferDisplayLayer. enqueue/flush are documented thread-safe and only the
pump thread drives it after start(), so assert that with nonisolated(unsafe).

cargo build/test/clippy/fmt green (core + host); xcframework rebuilt; swift build
+ iOS/tvOS targets clean with both warnings gone. Runtime confirmation of the
inversion warnings needs a GUI run under Xcode's Thread Performance Checker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:48:10 +02:00
enricobuehler bbabc04bca feat(hdr): Windows HDR10 + 10-bit end-to-end, negotiated; non-blocking capture recovery
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m32s
android / android (push) Successful in 1m49s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m36s
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 5s
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 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 3s
deb / build-publish (push) Successful in 2m20s
flatpak / build-publish (push) Successful in 4m6s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m32s
Adds true HDR (BT.2020 PQ) and 10-bit (HEVC Main10) streaming, negotiated so an
8-bit/SDR client is never sent a stream it can't decode, plus a robust fix for the
capture losing the stream across a secure-desktop transition.

Protocol (punktfunk-core/quic.rs):
- Hello gains `video_caps` (VIDEO_CAP_10BIT / VIDEO_CAP_HDR), Welcome gains `bit_depth`,
  both as optional trailing bytes (back-compat). client-rs advertises 10-bit via
  PUNKTFUNK_CLIENT_10BIT; the connector advertises 0 for now (in-band detection drives
  the native clients). Regenerated punktfunk_core.h.

Windows host:
- 10-bit Main10: host enables it only when the client advertised VIDEO_CAP_10BIT AND
  PUNKTFUNK_10BIT is set; threaded through open_video → NVENC (profile Main10,
  pixelBitDepthMinus8).
- HDR: when the captured desktop is scRGB FP16 (R16G16B16A16_FLOAT, HDR on), copy it to
  an FP16 surface, composite the cursor there, convert scRGB → BT.2020 PQ 10-bit
  (R10G10B10A2) via a shader, and encode HEVC Main10 with the BT.2020/PQ colour VUI
  (ABGR10 input). Fixes the freeze + cursor-trail that came from feeding FP16 into the
  BGRA path. Reacts dynamically to the HDR toggle.
- Capture recovery: rebuild is now a single NON-BLOCKING attempt, throttled to ~4×/s,
  repeating the last good frame between attempts (format-tagged last_present). During a
  secure-desktop dwell SudoVDA's output is gone; the old blocking 12 s retry starved the
  send loop for seconds so the client timed out and disconnected — now the session stays
  fed (frozen) until the desktop returns. Also seeds a black frame on recovery.

Apple client (PunktfunkKit):
- Detects HDR in-band from the stream VUI (PQ transfer function), decodes to 10-bit P010,
  and presents via an rgba16Float + BT.2020 PQ CAMetalLayer with EDR; SDR path unchanged.
  Switches automatically on a mid-session HDR toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:28:52 +00:00
enricobuehler f5eae24c87 feat(apple): tabbed macOS Settings + stats-overlay placement/toggle + Stream menu
ci / rust (push) Failing after 42s
apple / swift (push) Successful in 54s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 32s
android / android (push) Successful in 1m47s
ci / bench (push) Successful in 1m35s
decky / build-publish (push) Successful in 11s
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 4s
deb / build-publish (push) Successful in 2m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m27s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m28s
docker / deploy-docs (push) Successful in 20s
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>
2026-06-15 22:11:39 +02:00
enricobuehler 5dcb72f5af feat(android): rename display name to "Punktfunk" + drop the Settings "Done" button
ci / rust (push) Failing after 40s
apple / swift (push) Successful in 54s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 31s
android / android (push) Successful in 1m55s
ci / bench (push) Successful in 1m44s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 5s
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 4s
deb / build-publish (push) Successful in 3m20s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m52s
- Display name capitalized: app_name (launcher label + permission dialogs) and the connect-screen
  header are now "Punktfunk". Package/applicationId/service names stay lowercase.
- Settings: removed the redundant "Done" button (the bottom tab bar is the navigation; system Back
  still returns to Connect). Dropped the now-unused imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:30:29 +02:00
enricobuehler 49cdafc042 feat(android): connect-screen redesign — Apple-style cards, FAB + bottom sheet, fixed status bar
Polish pass on the connect screen.

- Host cards: ElevatedCard with a colored letter-avatar (Apple-contact style), name + address, a
  colored status pill (Paired / PIN pairing / Trust on first use), and an overflow menu with Forget
  on saved hosts. Tapping a card connects. Unifies the old saved/discovered rows into one HostCard.
- Manual connect moved behind an "Add host" ExtendedFloatingActionButton that opens a
  ModalBottomSheet with the Host/Port form (the current M3 pattern) — declutters the list.
- Empty state when there are no saved/discovered hosts; single scrollable column; removed the
  "core ABI v2" footer.
- Status bar: enableEdgeToEdge driven explicitly dark (transparent bars + light icons) so the
  status/nav bars blend with our always-dark surface instead of showing a black band (the no-arg
  edge-to-edge had picked the system light/dark theme).

Verified live (emulator screenshots): cards render with avatars + status pills + Forget menu; the
FAB opens the bottom-sheet form; the status bar blends with light icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:30:29 +02:00
enricobuehler ecd7d4a7e3 feat(android): mic uplink + connect-screen redesign
ci / web (push) Successful in 29s
android / android (push) Successful in 1m50s
ci / bench (push) Successful in 1m42s
apple / swift (push) Successful in 53s
ci / rust (push) Successful in 1m4s
ci / docs-site (push) Successful in 31s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 4s
deb / build-publish (push) Successful in 3m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m1s
Microphone uplink (client → host's virtual mic, 0xCB) and a cleaner connect screen.

Mic (Rust-heavy, mirrors the audio playback path in reverse):
- crates/punktfunk-android/src/mic.rs: AAudio LowLatency **input** → realtime callback hands
  captured f32 to a channel → a worker thread Opus-encodes 20 ms stereo frames (48 kHz, VOIP,
  64 kbps) and calls NativeClient::send_mic. MicCapture owns the stream + encode thread (RAII stop).
- session.rs: SessionHandle gains a `mic` slot; nativeStartMic/nativeStopMic JNI (mirror of audio);
  stopped in Drop. NativeBridge: the two externs.
- Settings: a `micEnabled` flag + a Microphone toggle in SettingsScreen that requests RECORD_AUDIO
  (denied → stays off). StreamScreen starts the mic only if enabled AND the permission is held.

Connect-screen redesign:
- One scrollable Column (was a fixed centered layout that could clip with the new tab bar);
  host rows render via forEach (no nested LazyColumn). Colored section labels ("Saved hosts",
  "Discovered on the network", "Connect manually"), full-width host cards / fields / Connect button,
  a header + subtitle, and a muted footer.

Verified live (emulator pf_phone -> home-worker-2): toggling mic requests RECORD_AUDIO; with it
granted, a session sends mic frames (client "mic: sent=250 … peak=0.439" — real audio) and the host
logs "client datagram stream ended … mic=276". Redesigned screen confirmed via screenshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:05:25 +02:00
enricobuehler 14fe450b72 feat(android): bottom tab bar (Connect / Settings)
apple / swift (push) Successful in 53s
ci / web (push) Successful in 35s
ci / docs-site (push) Successful in 35s
ci / bench (push) Successful in 1m48s
deb / build-publish (push) Successful in 3m28s
decky / build-publish (push) Successful in 10s
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 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
ci / rust (push) Successful in 6m59s
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 5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m46s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m44s
docker / deploy-docs (push) Successful in 19s
android / android (push) Successful in 2m41s
Replace the ad-hoc screen switching with a Material3 bottom NavigationBar. Two top-level
destinations — Connect (Home icon) and Settings (gear) — persist across tab switches; the
immersive stream view is shown full-screen, outside the bar. Settings is now a tab, so its
button is dropped from the Connect screen.

- app/build.gradle.kts: + androidx.compose.material:material-icons-core (tab icons).
- MainActivity: Screen sealed interface -> Tab enum; App() wraps the tabs in a Scaffold with a
  NavigationBar bottomBar (streamHandle != 0 -> StreamScreen full-screen); ConnectScreen drops
  the onOpenSettings param + the Settings button.

Verified live (emulator): the bar renders with Connect/Settings; tapping a tab swaps content and
moves the selected indicator; the bar persists on both tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:39:55 +02:00
enricobuehler 9ff5951cb2 feat(android): saved-hosts list + unify trust key on address:port
A managed list of known/paired hosts on the connect screen — one-tap reconnect + forget —
and a fix for the discovered-vs-manual trust-key split.

- kit/security: KnownHostStore (replaces the fp-only PinStore) stores KnownHost{address, port,
  name, fpHex, paired} keyed by address:port, persisted as JSON in SharedPreferences. So a
  discovered and a manually-typed connection to the same host now share ONE trust record (the old
  PinStore keyed discovered hosts by the mDNS instance id, manual by host:port — pairing via one
  path wasn't seen by the other).
- MainActivity: connect() looks up trust by (address, port); on a successful TOFU or PIN pairing
  the host is saved (paired flag set for the PIN path). A "Saved hosts" section lists them (name,
  address:port · paired/trusted, fp) with tap-to-reconnect (silent, pinned) and a Forget button.

Verified live (emulator -> home-worker-2): pair -> host appears under "Saved hosts" as paired;
tap -> silent reconnect (new host session, no dialog); Forget -> removed. Trust now shared across
the discovered + manual paths by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:31:46 +02:00
enricobuehler 3bcc36c801 feat(android): native display resolution + Settings screen
apple / swift (push) Successful in 53s
android / android (push) Failing after 1m15s
ci / rust (push) Failing after 43s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 30s
ci / bench (push) Successful in 1m43s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 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 3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m53s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m44s
deb / build-publish (push) Successful in 6m52s
docker / deploy-docs (push) Successful in 22s
The connect mode was hardcoded to 720p60 — violating the "native client resolution, no
scaling" invariant. Derive the device's real display mode (landscape, long edge = width) and
add a Settings screen to tune the stream, mirroring the Linux/Apple clients.

- crates/punktfunk-android: nativeConnect gains bitrateKbps + compositorPref + gamepadPref
  (CompositorPref/GamepadPref wire bytes via from_u8); these were hardcoded Auto/Auto/0.
- app/Settings.kt: Settings (width/height/hz/bitrate/compositor/gamepad; 0 = native/auto) +
  a SharedPreferences store + nativeDisplayMode (Display.mode, landscape-swapped) +
  effectiveMode + the UI option tables.
- app/SettingsScreen.kt: dropdowns for resolution / refresh / bitrate / compositor / controller.
- MainActivity: App owns the settings + a Settings screen; ConnectScreen resolves the effective
  mode (Native = the display), shows it on the Connect button, and threads the prefs through
  nativeConnect.

Mic + codec selection deferred (mic uplink isn't wired yet; the decoder is HEVC-only).

Verified live (emulator pf_phone -> home-worker-2): default -> host mode=2400x1080@60 (the
emulator's native display, was 720p); Settings 1920x1080 + 20 Mbps + DualSense -> host
mode=1920x1080, requested_kbps=20000, gamepad=dualsense (host created a UHID DualSense).
Settings persist across screens; pinned reconnect stays silent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:14:01 +02:00
enricobuehler 8ab262f8f8 feat(trust): host-gated trust-on-first-use — PIN pairing mandatory by default
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 1m12s
ci / web (push) Successful in 29s
android / android (push) Failing after 1m49s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m48s
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 5s
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 19s
flatpak / build-publish (push) Failing after 3s
deb / build-publish (push) Failing after 2m43s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m22s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m20s
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>
2026-06-15 13:27:52 +02:00
enricobuehler b0df291ffe feat(android): pairing/identity — persistent identity, TOFU pinning, SPAKE2 PIN ceremony
apple / swift (push) Successful in 55s
ci / rust (push) Failing after 1m11s
ci / web (push) Successful in 28s
android / android (push) Failing after 1m55s
ci / docs-site (push) Successful in 33s
ci / bench (push) Successful in 1m45s
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 6s
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 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m43s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m9s
docker / deploy-docs (push) Successful in 18s
M4 Android stage 1 (trust). The client now presents a persistent self-signed identity on
every connect, pins host certs trust-on-first-use, and runs the SPAKE2 PIN pairing
ceremony — parity with the Apple/Linux clients. The Rust connector already exposed this;
this wires it through the JNI + a Keystore-backed Kotlin store + the connect UI.

- crates/punktfunk-android: nativeGenerateIdentity (mint), nativeConnect gains
  certPem/keyPem/pinHex (identity + TOFU/pinned), nativeHostFingerprint, nativePair
  (SPAKE2). hex32/parse_hex32 helpers.
- kit/security: IdentityStore (AndroidKeyStore AES-256-GCM-wrapped PEM blob; StrongBox
  with TEE fallback; four-state load so a decrypt failure never shadow-mints), PinStore
  (host-id -> fp-hex in SharedPreferences). obtainIdentity mints once on genuine first run.
- app: ConnectScreen loads/mints the identity, looks up the stored pin, and gates connect
  on a trust decision — TOFU prompt (first connect), fingerprint-changed warning, PIN dialog.
- AndroidManifest: allowBackup=false (Keystore keys don't restore; a restored device
  re-mints rather than carrying a dead blob).

Verified live (emulator -> home-worker-2, synthetic m3-host):
- identity: host logs the presented client fingerprint; stable across an app restart.
- TOFU: first-connect prompt -> Trust -> pins the observed host fp -> pinned reconnect
  skips the prompt.
- SPAKE2: PIN ceremony -> "pairing complete — client trusted" -> auto-connect under
  --require-pairing; wrong PIN / host down -> "Pairing failed".

Known follow-up: trust is keyed by mDNS instance id for discovered hosts but by
"host:port" for manually-typed ones, so pairing via one path isn't recognized by the other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:29:04 +02:00
enricobuehler 3167c936c0 feat(android): mDNS host discovery (NsdManager) in the connect screen
apple / swift (push) Successful in 53s
ci / rust (push) Successful in 1m12s
android / android (push) Failing after 1m42s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m44s
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 5s
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 4s
flatpak / build-publish (push) Failing after 1s
deb / build-publish (push) Failing after 2m45s
docker / deploy-docs (push) Successful in 5s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m49s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m0s
M4 Android stage 1 (discovery). Kotlin-only — browse _punktfunk._udp and present a
tappable host list above the manual Host/Port fields.

- clients/android/kit: HostDiscovery — NsdManager browse + resolve (registerServiceInfoCallback
  on API 34+ for reliable TXT, legacy resolveService on 31-33), MulticastLock while running, and
  a pure parseTxt(proto/fp/pair/id). Exposes the live host set via an onChange callback (NSD
  callbacks land on the main thread). DiscoveredHost(name, host, port, fingerprint?, pairingRequired).
  + a JVM unit test of parseTxt.
- clients/android/app: ConnectScreen renders discovered hosts (tap -> fill host/port + connect);
  discovery scoped to the screen (start on enter, stop on connect/leave). Manifest adds
  CHANGE_WIFI_MULTICAST_STATE + ACCESS_WIFI_STATE (NEARBY_WIFI_DEVICES already declared). Trust
  stays TOFU (pin=None); fp shown advisory; pairingRequired shown (SPAKE2 PIN wiring is later).

Verified: parseTxt unit test (5/5 green); on the emulator a loopback NsdManager.registerService of
a fake _punktfunk._udp host was discovered + resolved + TXT-parsed and rendered as a card
(name/host:port/TOFU/fp) -- the full browse->resolve->parse->UI path. Real cross-LAN discovery
needs a physical device on the host LAN (the emulator's SLIRP NAT drops mDNS multicast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:32:09 +02:00
enricobuehler 104639bcc1 feat(android): DualSense host->client feedback — rumble + lightbar/LEDs/triggers
ci / docs-site (push) Successful in 29s
apple / swift (push) Successful in 54s
android / android (push) Failing after 1m39s
ci / rust (push) Failing after 1m44s
ci / web (push) Successful in 27s
ci / bench (push) Successful in 1m44s
decky / build-publish (push) Successful in 11s
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 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 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Successful in 3m10s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 1m18s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m31s
M4 Android stage 1 (DualSense feedback, host->client). Two Kotlin poll threads drain the
connector's rumble (0xCA) + HID-output (0xCD) planes via blocking native pulls and render
in Kotlin (Option B — no JNI upcalls, Android APIs stay in Kotlin).

- crates/punktfunk-android: feedback.rs — nativeNextRumble (returns (low<<16)|high, or -1)
  + nativeNextHidout (writes [kind][fields] into a caller's direct ByteBuffer). Ungated; no
  new Cargo deps (next_rumble/next_hidout are on the quic feature already).
- clients/android: GamepadFeedback.kt — rumble -> VibratorManager (two-motor amplitude),
  HID Led -> lightbar + PlayerLeds -> player LED via LightsManager (API 33+), adaptive
  triggers parsed + logged (no public Android API); resolves the connected pad, emulator ->
  logged no-op. Started/stopped in the StreamScreen lifecycle (stop + join before nativeClose).

Verified live (emulator -> synthetic host, PUNKTFUNK_TEST_FEEDBACK=1): client received +
decoded the full burst -- rumble low=16384 high=32768, Led r=10 g=20 b=30, PlayerLeds bits=4
player=1, Trigger which=1 mode=0x21 -- matching the host hook exactly. Rendering is a logged
no-op on the emulator (no controller); real haptics/lightbar/player-LED need a physical pad.
Deferred (need a physical DualSense + device enumeration): client->host rich input
(touchpad/motion send_rich_input) and DualSense controller-type negotiation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:30:32 +02:00
enricobuehler 1e871854cd feat(android): gamepad forwarding — buttons + sticks/triggers/dpad → send_input
apple / swift (push) Successful in 54s
android / android (push) Failing after 21s
ci / web (push) Failing after 12s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 1s
deb / build-publish (push) Failing after 0s
decky / build-publish (push) Failing after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 1s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 1s
docker / deploy-docs (push) Has been skipped
flatpak / build-publish (push) Failing after 0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1s
ci / rust (push) Failing after 2m35s
M4 Android stage 1 (gamepad). One controller forwarded as pad 0; mirrors the
Linux/Apple gamepad mapping (byte-identical GamepadButton/GamepadAxis events).

- crates/punktfunk-android: 2 JNI fns (nativeSendGamepadButton/Axis) building the
  GamepadButton/GamepadAxis InputEvents (flags = pad index 0).
- clients/android: Gamepad.kt — BTN_*/AXIS_* wire constants, KEYCODE_*->BTN_* map, and
  an AxisMapper (joystick MotionEvent -> sticks +-32767 +y-up / triggers 0..255 /
  HAT->BTN_DPAD_* with on-change gating + release-all reset). MainActivity routes
  gamepad-source KeyEvents in dispatchKeyEvent (DPAD only when from a gamepad, so
  keyboard arrows still map to VK) and adds dispatchGenericMotionEvent for joystick axes.

Verified live (emulator -> gamescope host, `adb input gamepad keyevent`): host created
the virtual X-Box 360 uinput pad (index=0) and received the gamepad datagrams (input=22).
Axes can't be adb-injected (joystick MotionEvents) -- build/clippy + code-review this
increment; live stick/trigger test deferred to a physical controller. Deferred: device
enumeration/selection, controller-type negotiation, DualSense rich input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:06:57 +02:00
enricobuehler ff1cc6c6d9 feat(android): input forwarding — keyboard + touch trackpad → send_input
ci / rust (push) Failing after 0s
ci / web (push) Failing after 0s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 1s
android / android (push) Failing after 0s
deb / build-publish (push) Failing after 0s
decky / build-publish (push) Failing after 1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / deploy-docs (push) Has been skipped
flatpak / build-publish (push) Failing after 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 40s
apple / swift (push) Successful in 53s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m21s
M4 Android stage 1 (input). Kotlin captures input and forwards it over JNI to
NativeClient::send_input (the connector is linked as a Rust crate).

- crates/punktfunk-android: 4 JNI send fns (pointer move / button / scroll / key)
  building InputEvent with the GameStream wire codes — ungated, &self on the Sync
  connector (safe from the UI thread).
- clients/android: Keymap.kt (Android KEYCODE_* -> Windows VK, the host's wire
  contract, mirroring the Linux/Apple tables); Activity-level dispatchKeyEvent forwards
  hardware keys to the active session (above the Compose focus system, so it's reliable);
  a Compose touch-trackpad overlay -- 1-finger drag -> relative move, tap -> left click,
  2-finger drag -> scroll.

Verified live (emulator -> gamescope host on the LAN box, synthetic `adb input`): host
received 31 input datagrams (input=31) and libei injected KeyDown/KeyUp, MouseButtonDown/Up
and MouseMove all emitted=true. Physical-mouse pointer capture + gamepad are next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:49:27 +02:00
enricobuehler 01305c67a7 fix(apple/gamepad): resolve DualSense type reliably at connect (no Auto race)
apple / swift (push) Successful in 54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 1s
docker / deploy-docs (push) Has been skipped
android / android (push) Failing after 0s
ci / rust (push) Failing after 1s
ci / web (push) Failing after 0s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 0s
deb / build-publish (push) Failing after 0s
decky / build-publish (push) Failing after 1s
flatpak / build-publish (push) Failing after 0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 0s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 0s
The DualSense intermittently showed up as an Xbox 360 pad on the host: the
client's `.auto` gamepad-type resolution read `GamepadManager.active`, which is
populated only by the async `.GCControllerDidConnect` notification (or the
init-time snapshot). At connect time `active` could still be nil with a DualSense
attached, so the client sent `.auto` and the host's pick_gamepad mapped that to
Xbox 360. Confirmed live: same box, two connects minutes apart logged
`gamepad="xbox360"` (auto) vs `honoring client gamepad request gamepad="dualsense"`.

resolveType() now calls rebuild() first to re-read GCController.controllers()
synchronously before reading `active`, closing the race for the common case
(controller attached before connecting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:31:39 +00:00
enricobuehler 8c8d576e52 feat(android): host→client audio — Opus → AAudio (LowLatency)
apple / swift (push) Successful in 53s
android / android (push) Failing after 1m21s
ci / rust (push) Failing after 1m32s
ci / web (push) Successful in 27s
ci / docs-site (push) Successful in 30s
decky / build-publish (push) Successful in 11s
ci / bench (push) Successful in 1m46s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 3m13s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 1m20s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 1m43s
M4 Android stage 1 (audio). An audio thread pulls Opus packets from the connector
(next_audio), decodes to interleaved f32 stereo, and feeds AAudio via its realtime
data callback through a jitter ring ported from the Linux client (prime ~3 quanta,
drop-oldest cap, re-prime on drain). All in Rust on native threads — symmetric with
the video decode path.

- crates/punktfunk-android: audio.rs (Opus decode + jitter ring + AAudio callback);
  SessionHandle gains an audio slot; nativeStartAudio/nativeStopAudio JNI; Drop stops it.
  Android-only deps: opus 0.3 (libopus via cmake, static) + ndk "audio" (AAudio) — pure
  C/NDK, no libc++_shared to bundle.
- clients/android: NativeBridge start/stop audio, called in the SurfaceView lifecycle.
- kit/build.gradle.kts: cargo-ndk env for the libopus cmake build (NDK root, Ninja,
  LIBOPUS_STATIC/NO_PKG) + --platform 31 (libaaudio is API 26+).

Verified live (emulator -> gamescope host on the LAN box): AAudio opened 48k/stereo/f32;
a 440 Hz tone played into the host capture sink reached the client decoded -- opus ~200/s,
pcm_frames climbing in lockstep, peak=0.089 (real content, not silence), with video
streaming concurrently. Some underruns under emulator jitter (verify on hardware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:25:24 +02:00
enricobuehler de7b8ac282 feat(android): video decode pipeline — NDK AMediaCodec → SurfaceView
apple / swift (push) Successful in 53s
ci / rust (push) Failing after 55s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 33s
android / android (push) Successful in 2m25s
ci / bench (push) Successful in 1m37s
decky / build-publish (push) Successful in 13s
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 4s
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 4s
flatpak / build-publish (push) Failing after 1s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 3m49s
deb / build-publish (push) Successful in 5m55s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m38s
docker / deploy-docs (push) Successful in 8s
M4 Android stage 1 (video). Pull HEVC access units from the connector and render
them to the SurfaceView entirely in Rust (NDK AMediaCodec → ANativeWindow) — no
per-frame JNI, honoring the native-thread hot-path invariant.

- crates/punktfunk-android: decode.rs (one-in/one-out AMediaCodec loop; in-band
  VPS/SPS/PPS so no out-of-band csd; dims from NativeClient::mode). SessionHandle
  now holds an Arc<NativeClient> + the decode thread; nativeStartVideo/nativeStopVideo.
- clients/android: connect screen (host/port) + full-screen SurfaceView stream
  screen — surfaceCreated -> nativeStartVideo, leaving -> stop + close.

Verified live (Android emulator -> m3-host on the LAN box, ABI v2): QUIC handshake,
8-round clock-skew sync, HEVC decoder configured at 1280x720, and the data plane
delivered + fed all 299 access units (the punktfunk/1 NAT hole-punch worked through
the emulator's SLIRP). Real-pixel render is pending a non-synthetic source:
`m3-host --source synthetic` emits dummy transport payloads (not HEVC), so the
decoder correctly produces nothing; `--source virtual` (a compositor on the host)
is needed to verify decode-to-screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:03:48 +02:00
enricobuehler 8956bc14de feat(packaging/flatpak,decky): Steam Deck client flatpak + plugin deploy + CI
apple / swift (push) Successful in 53s
android / android (push) Successful in 3m48s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 34s
ci / rust (push) Successful in 2m21s
ci / bench (push) Successful in 1m36s
decky / build-publish (push) Successful in 31s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
flatpak / build-publish (push) Failing after 4s
deb / build-publish (push) Successful in 2m38s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m9s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m42s
docker / deploy-docs (push) Successful in 16s
Ship the punktfunk Linux client to the Steam Deck as a Flatpak — the only viable
SteamOS install path, since /usr is read-only and lacks libadwaita/SDL3 — and
publish both it and the Decky plugin through Gitea. Built and validated live on a
Steam Deck (SteamOS 3.7): bundle installs user-scope, all libs resolve, libavcodec
resolves to the codecs-extra HEVC build, devices=all for DualSense hidraw.

packaging/flatpak (new):
- io.unom.Punktfunk.yml on GNOME 50 / freedesktop-sdk 25.08. rust-stable//25.08
  (rustc 1.96 — the GTK4 chain needs >=1.92; the EOL GNOME-48/24.08 rust-stable at
  1.89 could not build it) + llvm20 (libclang for bindgen in ffmpeg-sys-next/sdl3-sys).
  HEVC libavcodec comes from the runtime's auto codecs-extra extension point (no
  app-side codec declaration). Bundled SDL3 3.4.10 (matches sdl3-sys 0.6.6+SDL-3.4.10).
  finish-args: wayland/fallback-x11, --device=all (GPU/VAAPI + evdev + hidraw — flatpak
  cannot bind /dev/hidrawN char devices via --filesystem), pulseaudio, network,
  ~/.config/punktfunk.
- metainfo.xml, desktop, square SVG icon, build-flatpak.sh (offline cargo-sources;
  on-Deck org.flatpak.Builder or CI), README.

clients/decky:
- add LICENSE (MIT), fix package.json license (BSD-3-Clause -> Apache-2.0 OR MIT),
  add scripts/{package.sh,deploy.sh} (the plugins dir is root-owned: stage to /tmp,
  sudo install, restart plugin_loader), align the launcher fallback to the real
  flatpak app id io.unom.Punktfunk, rewrite the install section.

.gitea/workflows:
- flatpak.yml: privileged Fedora container builds the bundle and publishes to the
  Gitea generic registry (+ release attachment on tags).
- decky.yml: pnpm build -> store-layout zip -> registry (stable latest/ URL for
  Decky "install from URL").

docs: packaging/README + packaging/flatpak/README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:43:35 +02:00
enricobuehler 79217eb93d feat(android): scaffold the native Android client (Rust-heavy JNI bridge)
apple / swift (push) Successful in 52s
ci / docs-site (push) Successful in 27s
android / android (push) Successful in 4m52s
ci / web (push) Successful in 26s
ci / bench (push) Successful in 1m33s
ci / rust (push) Successful in 6m56s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
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 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 4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m54s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m29s
deb / build-publish (push) Successful in 6m46s
docker / deploy-docs (push) Successful in 22s
Rust-heavy client model (like punktfunk-client-linux): a new cdylib crate
crates/punktfunk-android links punktfunk-core and exposes the JNI seam;
Kotlin (clients/android) owns only the Android-framework surface. Kotlin can't
import the C header the way Swift can, so the bridge is written in Rust to reuse
the Linux client's orchestration rather than re-port it.

- crates/punktfunk-android: JNI bridge — abiVersion/coreVersion native-link
  proof + session connect/close handle; plane pumps stubbed for M4 stage 1.
- clients/android: Gradle project — :app (Compose) + :kit (Android library with
  a cargo-ndk Exec task -> jniLibs). AGP 9.2 / Gradle 9.4.1 / Kotlin 2.3.21 /
  Compose BOM 2026.05.01 / compileSdk 37 / targetSdk 36 / minSdk 31, shipping
  arm64-v8a + x86_64. Phone + TV (leanback) installable. README rewritten.
- .gitea/workflows/android.yml: CI mirroring apple.yml on a Linux runner.
- punktfunk-core: switch rcgen to the ring backend so the whole quic tree is
  aws-lc-free (smaller client .so, cmake-free cross-compile; a win for all targets).

Validated on this box: :app:assembleDebug -> APK with both ABIs; emulator
first-light renders the bridge linked (core ABI v2) with logcat confirmation;
clippy -D warnings + cargo fmt clean; core tests green on the ring backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:38:35 +02:00
enricobuehler a59abe2e3e fix(apple/gamepad): reclaim the PS/Home button from the macOS system gesture
ci / docs-site (push) Successful in 31s
ci / rust (push) Successful in 6m30s
deb / build-publish (push) Successful in 3m58s
ci / web (push) Successful in 27s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m13s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m18s
docker / deploy-docs (push) Successful in 17s
The earlier buttonHome handler wasn't enough: on macOS the SYSTEM grabs the DualSense
Home/PS button by default (opens Launchpad's Games folder), so it never reached the app.
The fix is to disable the system gesture on the element —
`physicalInputProfile.buttons[GCInputButtonHome].preferredSystemGestureState = .disabled`
(Apple's documented mechanism) — which hands the button to us.

Then drive `guide` DIRECTLY from that element's pressedChangedHandler instead of via
buttonMask: the legacy `extendedGamepad.buttonHome` is unreliable/often nil even when the
physical element exists, so reading it in the mask dropped presses. `sendGuide` folds the
bit into `buttons` so a held PS button still releases on focus loss. On tvOS the element
is reserved (nil) → the block no-ops.

The host already maps BTN_GUIDE → the DualSense PS bit, so this completes the chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:05:24 +00:00
enricobuehler 36107018a8 feat(apple/library): mTLS — authenticate by the paired identity, drop the token
apple / swift (push) Successful in 1m16s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 29s
ci / bench (push) Successful in 1m40s
ci / rust (push) Successful in 6m42s
deb / build-publish (push) Successful in 3m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m22s
docker / deploy-docs (push) Successful in 17s
Phase 3: the Apple library now talks to the host's HTTPS mgmt API (b4a85a8) over mTLS
using this client's persistent identity — the SAME cert the host paired over QUIC — so
there is NO manual token anymore.

- ClientTLS: builds a SecIdentity from the stored PEM (CryptoKit parses the rcgen P-256
  PKCS#8 key → x963 → SecKey; the cert PEM → SecCertificate; SecIdentityCreateWithCertificate
  pairs them via the Keychain). macOS-only for now (that API is unavailable on iOS — a
  PKCS#12 path would be needed there; the client is macOS-first).
- LibraryTLSDelegate: pins the host's self-signed cert by the fingerprint the client
  already trusts, and presents the identity for the client-cert challenge.
- LibraryClient.fetch now does GET https://…/library with the identity + host fingerprint;
  the whole connection form (port + token) and StoredHost.mgmtToken/setMgmt are gone — the
  library "just works" for a paired host. 401 → "pair with the host first".

Can't compile Swift on the Linux box; CI (apple.yml) compiles the macOS path incl. the
Security/CryptoKit code. Runtime (SecIdentity build + the mTLS handshake) needs Mac
validation. Pairs with the host mTLS already landed + live-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:47:19 +00:00
enricobuehler 8c2e245c8b fix(apple/cursor): disable the client-side cursor (gamescope traps input)
ci / docs-site (push) Successful in 31s
ci / web (push) Successful in 29s
apple / swift (push) Successful in 1m16s
ci / rust (push) Successful in 2m9s
ci / bench (push) Successful in 1m36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 3s
deb / build-publish (push) Successful in 2m24s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m54s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m26s
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>
2026-06-14 17:14:57 +00:00
enricobuehler 36a04e667c fix(apple): capture the PS/Home button + fullscreen only while streaming
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
ci / rust (push) Successful in 2m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 5s
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 4s
deb / build-publish (push) Successful in 2m26s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m53s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m21s
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>
2026-06-14 16:14:37 +00:00
enricobuehler 5706e7ebf4 feat(apple/library): launch a picked title (step 4 client side)
apple / swift (push) Successful in 1m17s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 30s
ci / rust (push) Successful in 2m2s
ci / bench (push) Successful in 1m34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 4s
deb / build-publish (push) Successful in 2m4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m10s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m13s
docker / deploy-docs (push) Successful in 17s
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 (27e5865).

- PunktfunkConnection.init gains `launchID` and calls the new punktfunk_connect_ex4
  (wrapping it in withOptionalCString; nil = host default).
- Threaded SessionModel.connect(launchID:) → ContentView.connect(_:launchID:) →
  a `launchTitle(host, id)` helper that dismisses the browser and connects.
- LibraryView gains `onLaunch`; cards become buttons that fire it. Wired on every
  platform (ContentView sheet on macOS/iOS, HomeView destination on tvOS) via a new
  `onLaunchTitle` closure on HomeView. Settings footer updated (launch is live now).

Can't compile Swift on the Linux box; CI (apple.yml) verifies. The host side of this
chain is live-validated on the dev box: a client `--launch custom:<id>` made the host
resolve the id and spawn gamescope running the title (see 27e5865).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:00:58 +00:00
enricobuehler 1b610d6bf5 feat(apple/library): experimental game-library browser (flagged off)
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m15s
ci / rust (push) Successful in 2m4s
ci / bench (push) Successful in 1m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
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 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 4s
deb / build-publish (push) Successful in 2m23s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m28s
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>
2026-06-14 14:28:16 +00:00
enricobuehler b3f98a5d7d feat(clients/decky): SteamOS Gaming-Mode launcher plugin (spike)
ci / rust (push) Successful in 2m7s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 29s
apple / swift (push) Successful in 1m15s
ci / bench (push) Successful in 1m35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 4s
deb / build-publish (push) Successful in 2m20s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m52s
docker / deploy-docs (push) Successful in 16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m23s
A Decky Loader plugin so a Steam Deck / SteamOS box can launch the punktfunk
client from Gaming Mode using REAL Steam UI components (it runs inside Steam's
CEF, so the panel is built from @decky/ui — the literal Big Picture primitives,
not a replica).

- Frontend (src/index.tsx, @decky/api + @decky/ui): a Quick Access Menu panel —
  Refresh → discover hosts, a native list (name, ip:port, pairing flag), tap to
  connect with a status toast, Disconnect.
- Backend (main.py): discover() shells `avahi-browse -rpt _punktfunk._udp` and
  parses the host's advertised TXT keys (proto/fp/pair/id from discovery.rs),
  dedup by id preferring IPv4; connect() resolves + spawns
  `punktfunk-client --connect host:port` (gamescope composites its video like a
  game), tracking the child; disconnect() terminates it.
- Mirrors the current official Decky template (the API moved to @decky/ui +
  @decky/api). Frontend builds clean (pnpm build → dist/index.js); main.py
  py_compiles. dist/ + node_modules gitignored — build on the Deck per README.

Spike scope: launcher only, runtime untested (no Deck here). Next on this track:
the in-stream Quick-Access overlay (volume/disconnect/stats over the running
stream) and a fuller real-components UI. Client decode on the AMD Deck is the
existing VAAPI path; the host-encode VAAPI gap is separate (NVIDIA host = NVENC).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:50:57 +00:00
enricobuehler c64816c70a feat(apple): client-side cursor for gamescope sessions (toggle + shortcut)
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m14s
ci / rust (push) Successful in 2m9s
ci / bench (push) Successful in 1m42s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
deb / build-publish (push) Successful in 2m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m51s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m24s
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, fc30307).
- Settings: "Cursor in stream" → Auto (gamescope) / Always / Never.
- Shortcut: ⌘⇧C toggles it live mid-session (re-engages capture so disassociation
  + abs/rel forwarding swap atomically); shown in the HUD.

macOS-only (the visible-cursor mode lives in the macOS StreamView). Verified to
compile + link via xcodebuild Release on the Mac; runtime behavior (cursor landing,
hover forwarding) to be confirmed live. Rust ABI side committed separately.

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