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:
@@ -434,7 +434,7 @@ public final class StreamLayerView: NSView {
|
||||
// Presenter choice — default stage-1 (the known-good AVSampleBufferDisplayLayer). Stage-2
|
||||
// (`punktfunk.presenter == "stage2"`) takes explicit VTDecompressionSession decode + a
|
||||
// CAMetalLayer/display-link present; it falls back here if Metal can't be set up.
|
||||
if UserDefaults.standard.string(forKey: "punktfunk.presenter") == "stage2",
|
||||
if UserDefaults.standard.string(forKey: DefaultsKey.presenter) == "stage2",
|
||||
let meter = presentMeter,
|
||||
let pipeline = Stage2Pipeline(presentMeter: meter) {
|
||||
startStage2(pipeline, connection: connection, onFrame: onFrame, onSessionEnd: onSessionEnd)
|
||||
@@ -455,17 +455,22 @@ public final class StreamLayerView: NSView {
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)?, onSessionEnd: (@Sendable () -> Void)?
|
||||
) {
|
||||
let metal = pipeline.layer
|
||||
displayLayer.addSublayer(metal) // contentsScale + frame set in layoutMetalLayer()
|
||||
// The opaque metal layer composites OVER the AVSampleBufferDisplayLayer base, which sits
|
||||
// idle (un-enqueued) in stage-2. contentsScale + frame are set in layoutMetalLayer().
|
||||
displayLayer.addSublayer(metal)
|
||||
metalLayer = metal
|
||||
stage2 = pipeline
|
||||
layoutMetalLayer()
|
||||
let link = displayLink(target: self, selector: #selector(stage2Tick(_:)))
|
||||
// Weak-proxy target so the link doesn't form a retain cycle with the view (see
|
||||
// DisplayLinkProxy) — the link retains the proxy; the proxy holds the view weakly.
|
||||
let proxy = DisplayLinkProxy { [weak self] link in self?.stage2Tick(link) }
|
||||
let link = displayLink(target: proxy, selector: #selector(DisplayLinkProxy.tick(_:)))
|
||||
link.add(to: .main, forMode: .common)
|
||||
stage2Link = link
|
||||
pipeline.start(connection: connection, onFrame: onFrame, onSessionEnd: onSessionEnd)
|
||||
}
|
||||
|
||||
@objc private func stage2Tick(_ link: CADisplayLink) {
|
||||
private func stage2Tick(_ link: CADisplayLink) {
|
||||
stage2?.renderTick(
|
||||
targetPresentNs: Stage2Pipeline.realtimeNs(forDisplayLinkTimestamp: link.targetTimestamp))
|
||||
}
|
||||
@@ -523,6 +528,7 @@ public final class StreamLayerView: NSView {
|
||||
appObservers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||
windowObservers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||
pump?.stop()
|
||||
teardownStage2() // invalidate the display link + stop the pipeline if stop() was missed
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user