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>
This commit is contained in:
2026-06-12 16:30:08 +02:00
parent c8099c0125
commit 9e8135ccec
12 changed files with 86 additions and 28 deletions
@@ -43,6 +43,9 @@ private final class FeedbackStopFlag: @unchecked Sendable {
/// amplitude and torn down on retarget; players run only while their motor is on, so an
/// idle controller costs no radio traffic. Failures (pads without haptics, engine resets)
/// downgrade to silence rumble is best-effort by design.
///
/// `@unchecked Sendable` is sound because every property (`controller`/`low`/`high`/`broken`) is
/// read and written only inside `queue` closures the serial queue is the synchronization.
private final class RumbleRenderer: @unchecked Sendable {
private let queue = DispatchQueue(label: "io.unom.punktfunk.haptics", qos: .userInteractive)
@@ -177,6 +180,11 @@ public final class GamepadFeedback {
}
}
/// Safety net: the drain thread captures `connection` strongly and only `self` weakly, so if
/// this is dropped without `stop()` (an abrupt teardown) the thread would poll forever and
/// leak the connection signal it to exit. (`stop()` is the normal path and also joins it.)
deinit { flag.stop() }
/// Map the DualSense player-LED bit patterns (5 LEDs, hid-playstation's player
/// conventions) onto GCControllerPlayerIndex. Unknown patterns fall back to the lit
/// count, clamped to the four indices GC offers.