Commit Graph

18 Commits

Author SHA1 Message Date
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 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
enricobuehler 9e8135ccec refactor(apple): code-quality pass — audit fixes + centralized defaults keys
A 6-agent adversarial audit of the client (11 confirmed of 39 findings, the rest
filtered) drove these:

- fix: SessionAudio ring buffer — guard a write larger than the ring (would push
  readIdx past writeIdx and corrupt the buffer; never happens, but guard not corrupt).
- fix: CADisplayLink retain cycle (stage-2 presenter) — a weak-target DisplayLinkProxy
  so the view can deallocate (the link retains its target); stage-2 teardown added to
  both StreamView/StreamViewController deinits as a safety net.
- fix: GamepadFeedback deinit { flag.stop() } — the drain thread holds the connection
  strongly and self weakly, so an abrupt teardown without stop() would leak it.
- refactor: centralize the 12 UserDefaults/@AppStorage key literals (scattered across
  8 files) into one DefaultsKey enum — a typo silently splits a setting's reader from
  its writer.
- docs: RumbleRenderer @unchecked Sendable invariant; the HID digit-row table; the
  stage-2 layer compositing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:30:34 +02:00
enricobuehler 7b10714b62 feat(apple): stage-2 presenter — explicit decode + Metal present + glass-to-glass
ci / web (push) Failing after 38s
ci / rust (push) Successful in 53s
docker / build-push (., web/Dockerfile, punktfunk-web) (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 16s
ci / docs-site (push) Failing after 39s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m17s
Opt-in (Settings -> Presenter; `punktfunk.presenter`, default stage-1). Stage-1's
AVSampleBufferDisplayLayer decodes AND presents internally with no per-frame
callback, so neither decode nor present can be stamped or hand-paced. Stage-2
takes explicit control:

- VideoDecoder: VTDecompressionSession, async output callback stamps
  decode-completion, session rebuilt on every IDR / format change. Unit-tested
  (testVideoDecoderAsyncCallbackDeliversPixels).
- MetalVideoPresenter: CAMetalLayer + CVMetalTextureCache + a runtime-compiled
  BT.709 limited-range NV12->RGB shader, present at the next vsync. The
  CVMetalTextures + pixel buffer are held until the GPU completes.
- Stage2Pipeline: pump thread -> decoder -> newest-ready 1-slot ring; the hosting
  view's display link drains it once per vsync and stamps capture->present
  (the display-link target time projected into CLOCK_REALTIME).
- LatencyMeter gains record(ptsNs:atNs:offsetNs:); the HUD shows a capture->present
  (glass-to-glass, modulo host render->capture) line, skew-corrected via
  clockOffsetNs. Measured live ~11 ms p50 vs ~2.2 ms capture->client.
- StreamView / StreamViewIOS host the CAMetalLayer as a sublayer + a CADisplayLink
  (NSView.displayLink on macOS) when stage-2; input capture + HUD unchanged. The
  session-active gates switch from `pump != nil` to `connection != nil` so capture
  engages without a StreamPump.

Validated: builds macOS/iOS/tvOS; the decode half is unit-tested; the Metal
present is live-validated on glass (correct image + the capture->present number).
Colorspace is BT.709 SDR for now; 10-bit/HDR + a pacing policy are later.
Plan: docs-site/content/docs/apple-stage2-presenter.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:29:23 +02:00
enricobuehler 6b4de5d738 feat(client/speedtest): request the host's full 3 Gbps probe ceiling
The Apple speed test asked for only 400 Mbps, capping the measured throughput
there and hiding the link's real headroom. Request the host's full
MAX_PROBE_KBPS (3 Gbps) instead, and raise the recommended-bitrate clamp from
500 Mbps to the host's 2 Gbps session ceiling so a fast measurement yields a
usable recommendation.

Also fix the stale caps left when the host clamps were raised (b8a33e2): the
resolved-bitrate range and the probe doc comments (abi.rs, client.rs,
regenerated header), plus the section 9 roadmap copy, now read 3 Gbps probe /
2 Gbps session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:08:19 +02:00
enricobuehler 1d605fb781 feat(gamepad): controller discovery + client-negotiated pad type + rich DualSense end to end
The Apple client grows full gamepad support and punktfunk/1 learns to negotiate
the virtual pad type:

- Protocol: Hello carries a GamepadPref byte (offset 21, the same trailing-byte
  back-compat pattern as the compositor; echoed resolved in Welcome at 54).
  Host precedence: explicit client choice > PUNKTFUNK_GAMEPAD env > Xbox 360,
  DualSense (UHID) only where available. ABI: punktfunk_connect_ex2 +
  punktfunk_connection_gamepad (connect_ex delegates; ABI_VERSION stays 2 — the
  trailing byte IS the compat mechanism). punktfunk-client-rs gets --gamepad.

- Swift client: GamepadManager (app-lifetime discovery + selection — Settings
  lists every controller with capabilities/battery/"In use"; exactly ONE pad
  forwards as pad 0, auto = most recently connected, or pinned), GamepadCapture
  (snapshot-diff button/axis events, DualSense touchpad + ~250 Hz motion on the
  rich-input plane, held state released on switch/deactivate/stop),
  GamepadFeedback (rumble → CoreHaptics per-handle engines; lightbar →
  GCDeviceLight; player LEDs → playerIndex; adaptive-trigger blocks → the
  table-driven DualSenseTriggerEffect parser → GCDualSenseAdaptiveTrigger,
  exact for the 10-zone positional modes). The pad type auto-resolves from the
  physical controller at connect time, user-overridable in Settings.

- Host DualSense fixes surfaced by adversarial review against hid-playstation /
  SDL / Nielk1 ground truth: input-report sensor/touch offsets were off by one
  (the kernel read garbage motion + phantom touches), the L2/R2 trigger blocks
  were swapped (the report is right-trigger-first), feedback now gates on the
  report's valid-flags (a plain rumble write no longer blanks lightbar/
  triggers), and the touchpad rescale clamps to the advertised ABS_MT extents.

- Tests: Hello/Welcome trailing-byte back-compat, pick_gamepad precedence,
  byte-exact input-report layout, valid-flag gating, per-mode trigger-parser
  table (incl. packed 3-bit zones), wire conversions, and a scripted loopback
  feedback burst (PUNKTFUNK_TEST_FEEDBACK=1) asserted through the xcframework
  on the rumble + HID-output planes.

Validated: cargo test/clippy/fmt green on macOS + Linux (61 host tests), swift
build/test green, test-loopback.sh green, tvOS/iOS targets compile. DualSense
motion sign/scale is derived from the calibration blob, not yet live-verified
(constants isolated in GamepadWire).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:28:33 +02:00
enricobuehler 7655c36f34 fix(apple/tvOS): hand-rolled selection screens — kills the black-text flash in pickers
ci / rust (push) Has been cancelled
The navigationLink Picker's INTERNAL destination list renders its rows in the focused
(dark-text) style while the push animates — black text over the dark backdrop until
focus settles (present under the old fade too; a SwiftUI-on-tvOS quirk we don't
control). Settings now uses its own primitives instead:

- TVSelectionRow: label + current value, pushes…
- TVSelectionList: a Settings-app-style option list (plain button rows + checkmark,
  selecting pops back) — ordinary button chrome, no focused-style pre-rendering.

The stream-mode and compositor pickers are gone on tvOS; the Settings screen itself is
a plain scroll of rows + footer (no Form), matching the rest of the tv UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:19:54 +02:00
enricobuehler f01b07a973 fix(apple/tvOS): pushed routes instead of modal covers — the Settings-app navigation feel
ci / rust (push) Has been cancelled
Add Host, Settings and PIN pairing were fullScreenCover overlays, which is why
navigating felt unlike the system Settings app (no push animation, no Menu-pops-a-level
semantics). They are now navigationDestination ROUTES pushed inside the home
NavigationStack:

- the system push/pop animation and Menu-button back navigation come for free;
- the Settings pickers' navigationLink pushes reuse the same stack (its inner
  NavigationStack wrapper is gone, as is the tvOS Done row — Menu pops, like Settings);
- Add Host is a real full-screen page (system navigation title, Settings-style rows on
  the standard backdrop) instead of a floating dialog, same for the pairing page;
- the thickMaterial cover backdrops became unnecessary and are gone. The system
  keyboard entries stay as covers — that presentation is system-owned either way.

iOS/macOS keep their sheets. Verified by screenshot: Add Host renders as a pushed
full-screen route with the title top-center.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:03:10 +02:00
enricobuehler 9e57a5a1ff fix(apple/tvOS): native form controls — pushed pickers, single-pill fields, centered values
ci / rust (push) Has been cancelled
The inline iOS form widgets fought the tvOS focus system at every turn: focused fields
showed nested pills, rows darkened oddly and grew on activation, the Compositor picker
was not even focusable, and prefilled fields (port, client name) floated their label
inside the pill, shoving the value off-center.

- Settings is now a fully tv-native screen: NO inline text entry — the stream mode is
  a preset picker (This TV native / 720p / 1080p / 4K, plus a Custom entry preserving
  a mode set on another platform) and both pickers use .navigationLink style (pushed
  selection lists, exactly like the system Settings app — and properly focusable; the
  cover wraps in a NavigationStack for the pushes).
- Where text entry is unavoidable (Add Host, PIN pairing), the fields keep their stock
  single-pill chrome (the grouped form style stays off tvOS — its row platters were
  one of the nested pills) and prefilled fields hide their floating label so values
  center vertically.
- All earlier row-clearing experiments reverted.

Verified by screenshot in the Apple TV simulator: Settings rows render as single
focus lozenges with chevrons; the Add Host pills are uniform with centered text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:38:37 +02:00
enricobuehler 7dd479f9e4 fix(apple/tvOS): television-idiomatic chrome — grid action tiles + full-screen covers
ci / rust (push) Has been cancelled
The iOS chrome half-worked on tvOS: toolbar items rendered tiny with clipped labels
and could not even be focused (which is why "+" never opened the add-host form), and
sheet presentations are not a tvOS idiom (the Settings form looked broken).

- The toolbar is gone on tvOS. Add Host and Settings live IN the hosts grid as
  full-size, focus-native tiles (.card style, same geometry as the host cards) — the
  natural way actions work on television.
- Every modal (Add Host, Settings, PIN pairing) presents as a fullScreenCover on tvOS;
  Settings gains a tvOS-only Done button (covers don't dismiss themselves).
- iOS/macOS keep their existing toolbar + sheets untouched.

Verified in the Apple TV simulator: title, host card and both action tiles render
full-size and focusable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:22:18 +02:00
enricobuehler bfd8c7be93 feat(apple): tvOS client — third app target, first-lit in the Apple TV simulator
ci / rust (push) Has been cancelled
The same app now runs on tvOS (target Punktfunk-tvOS, bundle io.unom.punktfunk.tvos),
validated live against the box: vkcube at 1280x720@60, 60 fps in the Apple TV 4K
simulator, glass HUD with a focusable Disconnect button.

- PunktfunkCore.xcframework grows tvOS device + universal-simulator slices. These are
  TIER-3 Rust targets (no prebuilt std): BUILD_TVOS=1 builds them with nightly and
  -Zbuild-std from rust-src — the full quic stack (quinn/rustls-ring/tokio) compiles
  for tvOS unchanged.
- The UIKit stream view covers iOS AND tvOS, with pointer interaction, pointer lock,
  touch forwarding and InputCapture gated to iOS — tvOS is view-only until gamepad
  capture lands (the natural tvOS input).
- SessionAudio on tvOS: .playback session, no mic (no app-accessible microphone).
- App chrome gates: keyboardShortcut/textSelection/controlSize/statusBarHidden are
  iOS/macOS-only; host cards use the focus-native .card button style on tvOS; the
  Audio settings section hides (system-routed); mode seeding works from the TV screen
  (1920x1080@60).
- Package platforms += .tvOS(.v17); new Xcode target + shared scheme
  (TARGETED_DEVICE_FAMILY 3, local-network usage description included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:10:40 +02:00
enricobuehler e1af4d57c6 feat(apple): iOS/iPadOS client — touch, pointer lock, shared SwiftUI shell
ci / rust (push) Has been cancelled
The whole client now runs on iPadOS/iOS from the same sources, first-lit live in the
iPad simulator against the real host at 1280x720@60 (60 fps on the HUD, capture state
machine active, mic permission flow shown).

- PunktfunkCore.xcframework grows iOS device + universal-simulator slices
  (BUILD_IOS=1; rustup targets aarch64-apple-ios{,-sim} + x86_64-apple-ios).
- The decode pump is extracted into a shared StreamPump (identical IDR re-gate logic on
  both platforms); the iOS StreamView (StreamViewIOS.swift) has the same name/signature
  as the macOS one, so ContentView & co. are byte-identical across platforms — hosted
  in a UIViewController for prefersPointerLocked (the iPadOS cursor capture; see README
  note 9 for the UIHostingController forwarding caveat).
- Touch is always forwarded: per-finger wire ids, coordinates mapped through the
  aspect-fit letterbox into LIVE host-mode pixels (surface == host mode, identity
  rescale host-side; follows mid-stream requestMode switches).
- InputCapture is cross-platform: GC works the same on iPadOS, ⌘⎋ is detected from the
  HID stream there; stale-⌘ tracking after focus loss fixed on both platforms
  (releaseAll now drops the modifier/latch state — a ⌘ released in another app
  otherwise hijacked Esc forever).
- SessionAudio: AVAudioSession on iOS (.playAndRecord + .defaultToSpeaker — without it
  iPhones route host audio to the EARPIECE; deactivated with
  notifyOthersOnDeactivation on stop so interrupted background audio resumes); HAL
  device pinning + the Settings pickers stay macOS-only.
- New Punktfunk-iOS app target (shared synchronized sources, generated Info.plist with
  mic + local-network usage descriptions — QUIC to a LAN host trips local network
  privacy on real devices — scene manifest + indirect input events for Stage Manager /
  external displays), shared scheme, macOS min-window frames gated off iOS.

For the iPad-on-an-external-screen idea: with multiple scenes + indirect input enabled,
Stage Manager iPads can drag the punktfunk window onto the external display and drive
the PC with keyboard/mouse/touch. Known gaps (README note 9): the pointer-lock
preference isn't consulted through UIHostingController (relative mouse works, the local
cursor just stays visible) and AVAudioSession interruptions don't auto-restart audio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:18:25 +02:00
enricobuehler b26f138699 feat(apple): session audio — host playback + mic uplink, device pickers in Settings
ci / rust (push) Has been cancelled
Both directions of the audio plane, on CoreAudio's built-in Opus codec
(kAudioFormatOpus — no bundled libopus; OpusCodec.swift, round trip unit-tested):

- Playback: a drain thread pulls nextAudio() packets, decodes, and writes a priming
  jitter ring feeding an AVAudioSourceNode (~20 ms prefill, adaptive to the device's
  render quantum so large-buffer devices don't oscillate prime/dropout; a high-water
  clamp sheds stall backlog so one network hiccup can't permanently lag audio behind
  video; underrun re-primes — one dip, not sustained crackle).
- Mic: a second engine taps the input device, resamples to 48 kHz stereo, Opus-encodes
  20 ms chunks and sendMic()s them into the host's virtual PipeWire source. Permission
  via AVCaptureDevice (NSMicrophoneUsageDescription added to the Xcode target).
- Settings: Speaker + Microphone pickers (CoreAudio HAL enumeration, persisted by
  device UID — "System default" leaves the engine unpinned so it follows macOS device
  changes) and a "Send microphone" toggle (default on). Applies from the next session.
- Audio starts with streaming, never during the trust prompt (no host sound — and no
  mic uplink — before the user trusted the host); teardown stops audio before close().

Adversarial-review fixes baked in: stop() and the dangling mic-permission callback
share one lock+flag protocol (no hot mic with no owner), the connect-success handler
bails when the attempt was abandoned mid-handshake (no session/mic for a dead window),
SessionAudio gets a deinit backstop (a dropped instance can't pin the connection via
its drain thread), and the render scratch buffer is block-owned (was leaked per
session).

Verified live against the box: remote test decodes 100 host Opus packets to PCM and
the host opens its virtual mic on the first uplinked frame ("punktfunk/1 virtual mic
ready"); on-glass session runs with both engines up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:39:15 +02:00
enricobuehler a9d1c16067 feat(apple): client-selectable compositor in the macOS client
ci / rust (push) Has been cancelled
Adopts punktfunk_connect_ex from the compositor-selection batch: a Compositor enum on
PunktfunkConnection (auto/kwin/wlroots/mutter/gamescope, with the host's name aliases
for env parsing), a "Host compositor" picker in Settings (default Automatic — a
concrete choice is honored only if that backend is available host-side), and
PUNKTFUNK_COMPOSITOR / PUNKTFUNK_REMOTE_COMPOSITOR pass-throughs for the autoconnect
dev hook and the remote first-light test. The wire change is backward-compatible
(optional trailing byte), so no behavior changes at the default.

Validated live against the box: host with no compositor env (auto-detect = KWin)
logged "honoring client compositor request compositor=gamescope" and streamed 60/60
decoded frames from the spawned gamescope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:51:42 +02:00
enricobuehler 5e77731da0 feat: hosts grid + trust-on-first-use UX + settings pane
ci / rust (push) Has been cancelled
The app grows from a dev connect form into a real client shell:

- Home is a grid of saved hosts (UserDefaults-persisted; context menu: Remove / Forget
  Identity), "+" in the toolbar opens the add-host sheet, the stream mode moved into
  Settings (⌘, / gear) — native resolution stays the only mode, no scaling.
- Trust is now explicit: the protocol always supported certificate pinning, but the app
  passed no pin and discarded the observed fingerprint — silently trusting any host.
  First connect now shows the host's SHA-256 fingerprint (compare with the "clients pin
  this fingerprint" line in the host log) over the live-but-blurred stream; the stream
  must pump immediately (the opening IDR is the only guaranteed one), so StreamView gains
  a capturesCursor switch to keep the cursor free while the prompt needs clicking, and
  input capture starts only after confirmation. Trusting pins the fingerprint per host;
  a changed host identity then refuses to connect.
- PUNKTFUNK_AUTOCONNECT keeps working (auto-trusts, doesn't touch the saved hosts).

Host→client authorization (pairing PIN) remains a punktfunk-core roadmap item — the host
still accepts any client that can reach its port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:15:37 +02:00