diff options
Diffstat (limited to 'src/macos/Sources/AppDelegate.swift')
| -rw-r--r-- | src/macos/Sources/AppDelegate.swift | 63 |
1 files changed, 58 insertions, 5 deletions
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift index 0cecd5da..2d411873 100644 --- a/src/macos/Sources/AppDelegate.swift +++ b/src/macos/Sources/AppDelegate.swift @@ -22,6 +22,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { // call pump(), but none advances animation; they only observe this flag and // leave the already-scheduled frame alone. private var pumpScheduled: Bool = false + /// The display's own clock, when the OS will lend us one. + /// + /// The fallback below is a `DispatchQueue.asyncAfter(0.016)` chain re-armed + /// AFTER each frame's tick, drain and draw, so its real period is 16 ms plus + /// all of that — comfortably slower than 60 Hz, and jittery, and never in + /// phase with the refresh. That is what makes an animation look choppy even + /// when nothing is dropping frames. A display link fires once per refresh, + /// phase-locked to vsync, which is the cadence the picture is actually + /// presented at. + /// + /// Cadence is now only a SMOOTHNESS question: `pardes_animation_tick` + /// spends measured elapsed time, so a 120 Hz link does not double-speed an + /// animation and a slow one does not halve it. + /// Held as `AnyObject` because a stored property cannot carry + /// `@available`, and this file still deploys to macOS 13. + private var displayLink: AnyObject? // The core is a singleton with no "is it alive" query, and Finder can hand // us documents before applicationDidFinishLaunching runs. Every entry point // that would call into libpardes from outside the launch sequence checks @@ -585,25 +601,62 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { } private func scheduleAnimationFrame() { - guard pardes_animating() && !pumpScheduled else { return } + guard pardes_animating() else { + stopDisplayLink() + return + } + if #available(macOS 14.0, *) { + // One link for the whole animating run, not one callback armed per + // frame: re-arming after the work is what put the period at 16 ms + // PLUS the work, and no amount of tuning that constant fixes a + // clock that is not the display's. + if displayLink == nil { + let link = view.displayLink(target: self, selector: #selector(displayLinkFired)) + link.add(to: .main, forMode: .common) + displayLink = link + } + return + } + // macOS 13 has no NSView display link. Keep the old chain, which is now + // only a cadence compromise rather than a correctness one — the tick + // spends measured time either way. + guard !pumpScheduled else { return } let awaitingPresentation = view.presentationSerial pumpScheduled = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.016) { + self.pumpScheduled = false self.animationFrame(after: awaitingPresentation) } } + private func stopDisplayLink() { + if #available(macOS 14.0, *) { (displayLink as? CADisplayLink)?.invalidate() } + displayLink = nil + } + + @objc private func displayLinkFired() { + guard coreIsUp else { return } + animationFrame(after: pendingPresentation) + } + + /// The presentation serial the last dirtied sample is waiting on. Held + /// across display-link callbacks because the link, unlike the old chain, + /// does not carry it in a closure. + private var pendingPresentation: UInt64 = 0 + private func animationFrame(after awaitingPresentation: UInt64) { - pumpScheduled = false guard view.presentationSerial != awaitingPresentation else { // Keep the dirty sample pending. AppKit may have coalesced this - // draw or the window may be occluded; retry the clock without - // advancing until draw(_:) supplies the presentation permit. + // draw or the window may be occluded; retry on the next refresh + // without advancing, until draw(_:) supplies the presentation + // permit. On the link that costs ONE refresh; the old chain paid a + // fresh 16 ms for it, which is where the visible hitching came from. view.needsDisplay = true - scheduleAnimationFrame() + if #available(macOS 14.0, *) {} else { scheduleAnimationFrame() } return } _ = pardes_animation_tick() + pendingPresentation = view.presentationSerial pump() } |
