summaryrefslogtreecommitdiff
path: root/src/macos/Sources/AppDelegate.swift
diff options
context:
space:
mode:
Diffstat (limited to 'src/macos/Sources/AppDelegate.swift')
-rw-r--r--src/macos/Sources/AppDelegate.swift63
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()
}