fix(apple): default macOS PyroWave sessions to glass-gated present pacing — DCP swapID kernel-panic mitigation
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / bench (push) Successful in 7m29s
docker / deploy-docs (push) Successful in 26s
release / apple (push) Successful in 10m9s
arch / build-publish (push) Successful in 13m2s
android / android (push) Successful in 14m9s
deb / build-publish (push) Successful in 16m41s
apple / screenshots (push) Successful in 6m26s
ci / rust (push) Successful in 20m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m8s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
apple / swift (push) Successful in 1m15s
decky / build-publish (push) Successful in 20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / bench (push) Successful in 7m29s
docker / deploy-docs (push) Successful in 26s
release / apple (push) Successful in 10m9s
arch / build-publish (push) Successful in 13m2s
android / android (push) Successful in 14m9s
deb / build-publish (push) Successful in 16m41s
apple / screenshots (push) Successful in 6m26s
ci / rust (push) Successful in 20m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m8s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
apple / swift (push) Successful in 1m15s
decky / build-publish (push) Successful in 20s
Windowed PyroWave sessions were kernel-panicking Macs ("mismatched
swapID's" @UnifiedPipeline.cpp, WindowServer → AppleMobileDisp*-DCP).
The panic is an Apple DCP bug (SketchUp/Unity/240Hz-monitor reports hit
the same signature), but our stage-2 arrival pacing feeds the trigger
pattern: displaySyncEnabled=false out-of-band presents arriving faster
than the compositor latches them in a composited (windowed) session.
PyroWave makes that pattern routine — near-instant Metal wavelet decode
turns network clumps into same-millisecond present bursts, and it is
the codec that sustains stream rates above panel refresh.
Mitigation: PyroWave sessions on macOS now default to the stage-3
PresentGate (one presented-but-undisplayed swap in flight, serialized
on the on-glass callback, 100 ms stale backstop) — the racing pattern
cannot occur. Explicit stage2/stage3 picks (setting or
PUNKTFUNK_PRESENTER) still win, so the arrival-pacing A/B stays honest.
VideoToolbox codecs keep arrival pacing (decode latency spaces their
presents; no panic reports there).
PresenterChoice gains an `explicit` resolver (nil = no user selection)
so the codec-conditional default only applies when the user hasn't
picked a stage; pacing selection is a testable helper carrying the
rationale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,12 +42,20 @@ enum PresenterChoice: Equatable {
|
||||
/// leftover DEBUG "stage1" value silently maps to the default rather than reviving the
|
||||
/// freeze-prone fallback.
|
||||
static func resolve(setting: String?, env: String?, allowStage1: Bool) -> PresenterChoice {
|
||||
explicit(setting: setting, env: env, allowStage1: allowStage1) ?? platformDefault
|
||||
}
|
||||
|
||||
/// The user's EXPLICIT stage selection, nil when they haven't made one (unset/unknown values,
|
||||
/// and a release build's gated "stage1"). Split from `resolve` so a codec-conditional default
|
||||
/// (see `SessionPresenter.pacing`) can apply only when the user hasn't picked a stage — an
|
||||
/// explicit "stage2" must stay a faithful A/B of arrival pacing.
|
||||
static func explicit(setting: String?, env: String?, allowStage1: Bool) -> PresenterChoice? {
|
||||
let raw = env.flatMap { $0.isEmpty ? nil : $0 } ?? setting
|
||||
switch raw {
|
||||
case "stage1": return allowStage1 ? .stage1 : platformDefault
|
||||
case "stage1": return allowStage1 ? .stage1 : nil
|
||||
case "stage2": return .stage2
|
||||
case "stage3": return .stage3
|
||||
default: return platformDefault
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +74,31 @@ enum PresenterChoice: Equatable {
|
||||
}
|
||||
|
||||
final class SessionPresenter {
|
||||
/// Present pacing for this session. Stage-3 always means glass gating; under the stage-2
|
||||
/// default, macOS PyroWave sessions ALSO get glass gating — a kernel-panic mitigation, not a
|
||||
/// latency tweak. macOS's DCP panics ("mismatched swapID's" @UnifiedPipeline.cpp, the whole
|
||||
/// machine dies) when WindowServer's swap submissions race, and the reliable trigger is
|
||||
/// out-of-band CAMetalLayer presents (displaySyncEnabled=false — mandatory for us, see
|
||||
/// MetalVideoPresenter's init) arriving faster than the compositor latches them in a
|
||||
/// COMPOSITED (windowed) session. Arrival pacing does exactly that with PyroWave: the wavelet
|
||||
/// decode is near-instant Metal compute, so a network clump of frames presents within the
|
||||
/// same millisecond, and PyroWave is the codec that sustains stream rates above the panel's
|
||||
/// refresh. The glass gate admits one presented-but-undisplayed swap at a time (serialized on
|
||||
/// the on-glass callback, 100 ms stale backstop), which removes the racing pattern outright;
|
||||
/// frames the panel couldn't have shown anyway coalesce in the newest-wins ring. An explicit
|
||||
/// stage-2 pick (setting/env) still forces arrival pacing — that A/B lever must stay honest.
|
||||
/// VideoToolbox codecs keep arrival pacing: decode latency spaces their presents, and years
|
||||
/// of stage-2 defaults there predate any panic report.
|
||||
static func pacing(
|
||||
for choice: PresenterChoice, explicit: PresenterChoice?, codec: VideoCodec
|
||||
) -> PresentPacing {
|
||||
if choice == .stage3 { return .glass }
|
||||
#if os(macOS)
|
||||
if explicit == nil, codec == .pyrowave { return .glass }
|
||||
#endif
|
||||
return .arrival
|
||||
}
|
||||
|
||||
private var pump: StreamPump?
|
||||
private var stage2: Stage2Pipeline?
|
||||
private var stage2Link: CADisplayLink?
|
||||
@@ -112,15 +145,17 @@ final class SessionPresenter {
|
||||
#else
|
||||
let allowStage1 = false
|
||||
#endif
|
||||
let choice = PresenterChoice.resolve(
|
||||
let explicit = PresenterChoice.explicit(
|
||||
setting: UserDefaults.standard.string(forKey: DefaultsKey.presenter),
|
||||
env: ProcessInfo.processInfo.environment["PUNKTFUNK_PRESENTER"],
|
||||
allowStage1: allowStage1)
|
||||
let choice = explicit ?? PresenterChoice.platformDefault
|
||||
if choice != .stage1,
|
||||
let pipeline = Stage2Pipeline(
|
||||
endToEndMeter: endToEndMeter, decodeMeter: decodeMeter,
|
||||
displayMeter: displayMeter,
|
||||
pacing: choice == .stage3 ? .glass : .arrival) {
|
||||
pacing: Self.pacing(
|
||||
for: choice, explicit: explicit, codec: connection.videoCodec)) {
|
||||
let metal = pipeline.layer
|
||||
// The opaque metal layer composites OVER the AVSampleBufferDisplayLayer base, which
|
||||
// sits idle (un-enqueued) in stage-2. contentsScale + frame are set in layout().
|
||||
|
||||
@@ -122,6 +122,11 @@ private final class VsyncClock: @unchecked Sendable {
|
||||
/// coalesce in the newest-wins ring. Freshness is preserved by DROPPING stale frames before
|
||||
/// present instead of queueing them behind the display — the hidden queue latency becomes
|
||||
/// explicit, correct frame drops.
|
||||
///
|
||||
/// macOS PyroWave sessions default to `glass` even though the platform default is stage-2: burst
|
||||
/// presents into a composited (windowed) layer are the trigger pattern for the macOS DCP
|
||||
/// "mismatched swapID's" KERNEL PANIC, and the one-in-flight gate removes that pattern — see
|
||||
/// `SessionPresenter.pacing` for the full rationale.
|
||||
public enum PresentPacing: Sendable {
|
||||
case arrival
|
||||
case glass
|
||||
|
||||
Reference in New Issue
Block a user