Skip to content

Commit 7247148

Browse files
dskuzadskuza
andcommitted
Use updated CADisplayLink API for macOS 14+
Adds support for the updated `CADisplayLink` API available on macOS 14+, which allows for setting preferred fps / frame rate range. This also moves the `DisplayLinkProxy` into a protocol with concrete types for all platforms. ## Changes - [ ] Update `DisplayLinkProxy` to `RiveDisplayLink` protocol - Replace usage of proxy with new protocol - [ ] On macOS 14+, use `CADisplayLink` - Additionally adds support for preferred fps / frame rate range ## Notes While this is not "breaking", it appropriately changes the availability of some setters to match the API availability of the display link used. Some developers may find that there are errors - for example, setting FPS was unavailable on macOS, since `CVDisplayLink` didn't support it. Instead of no-oping, this function is now unavailable if the OS doesn't support it. Diffs= 34820199f2 Use updated CADisplayLink API for macOS 14+ (#8899) Co-authored-by: David Skuza <david@rive.app>
1 parent 23f714d commit 7247148

File tree

7 files changed

+252
-91
lines changed

7 files changed

+252
-91
lines changed

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
63642c62d46f41939545609dd151b68d0f4f67fe
1+
34820199f2bb9bee7b4e9d7284cbbd89a2816808

RiveRuntime.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
F21F08142C66526D00FFA205 /* RiveFallbackFontDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */; };
9797
F23626AA2C8F90FA00727D9A /* nested_text_run.riv in Resources */ = {isa = PBXBuildFile; fileRef = F23626A92C8F90FA00727D9A /* nested_text_run.riv */; };
9898
F23992E72CB9C1C60021EF61 /* RenderContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F23992E62CB9C1C60021EF61 /* RenderContextTests.m */; };
99+
F23B2B802D2DCD70007AA53E /* RiveDisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23B2B7F2D2DCD70007AA53E /* RiveDisplayLink.swift */; };
99100
F2610DD22CA5B4C40090D50B /* RiveLogger+StateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2610DD12CA5B4C40090D50B /* RiveLogger+StateMachine.swift */; };
100101
F2610DD42CA5B5460090D50B /* RiveLogger+Artboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2610DD32CA5B5460090D50B /* RiveLogger+Artboard.swift */; };
101102
F2610DD62CA5B5DD0090D50B /* RiveLogger+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2610DD52CA5B5DD0090D50B /* RiveLogger+ViewModel.swift */; };
@@ -217,6 +218,7 @@
217218
F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontDescriptor.swift; sourceTree = "<group>"; };
218219
F23626A92C8F90FA00727D9A /* nested_text_run.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = nested_text_run.riv; sourceTree = "<group>"; };
219220
F23992E62CB9C1C60021EF61 /* RenderContextTests.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RenderContextTests.m; sourceTree = "<group>"; };
221+
F23B2B7F2D2DCD70007AA53E /* RiveDisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveDisplayLink.swift; sourceTree = "<group>"; };
220222
F2610DD12CA5B4C40090D50B /* RiveLogger+StateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveLogger+StateMachine.swift"; sourceTree = "<group>"; };
221223
F2610DD32CA5B5460090D50B /* RiveLogger+Artboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveLogger+Artboard.swift"; sourceTree = "<group>"; };
222224
F2610DD52CA5B5DD0090D50B /* RiveLogger+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveLogger+ViewModel.swift"; sourceTree = "<group>"; };
@@ -401,6 +403,7 @@
401403
C3468E5727EB9887008652FD /* RiveView.swift */,
402404
C3468E5927ECC7C6008652FD /* RiveViewModel.swift */,
403405
C3468E5B27ED4C41008652FD /* RiveModel.swift */,
406+
F23B2B7F2D2DCD70007AA53E /* RiveDisplayLink.swift */,
404407
F21F08152C66527B00FFA205 /* Fonts */,
405408
C9BD3927263B64B100696C37 /* Renderer */,
406409
C3468E5627EB9858008652FD /* Utils */,
@@ -650,6 +653,7 @@
650653
046FB7F5264EAA60000129B1 /* RiveSMIInput.mm in Sources */,
651654
043026002AFA915B00320F2E /* RiveFileAsset.mm in Sources */,
652655
F2FD94042CC9492B00C1FC85 /* RiveFont.m in Sources */,
656+
F23B2B802D2DCD70007AA53E /* RiveDisplayLink.swift in Sources */,
653657
C3468E5A27ECC7C6008652FD /* RiveViewModel.swift in Sources */,
654658
C3745FD3282BFAB90087F4AF /* FPSCounterView.swift in Sources */,
655659
F2ECC2312C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift in Sources */,

Source/Renderer/include/rive_renderer_view.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
1818

1919
@interface RiveRendererView : RiveMTKView
2020

21+
@property(nullable, nonatomic, strong) id<CAMetalDrawable>currentDrawableOverride;
22+
2123
- (instancetype)initWithFrame:(CGRect)frameRect;
2224
/// Deprecated. Use `alignWithRect:contentRect:alignment:fit:scaleFactor:` instead.
2325
/// This is equivalent to calling the new function with a scale factor of 1.

Source/Renderer/rive_renderer_view.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ - (void)drawInRect:(CGRect)rect
349349
return;
350350
}
351351

352-
if (![[self currentDrawable] texture])
352+
if (!self.currentDrawable.texture)
353353
{
354354
return;
355355
}

Source/RiveDisplayLink.swift

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//
2+
// RiveDisplayLink.swift
3+
// RiveRuntime
4+
//
5+
// Created by David Skuza on 1/7/25.
6+
// Copyright © 2025 Rive. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
protocol RiveDisplayLink {
12+
var isPaused: Bool { get set }
13+
var targetTimestamp: TimeInterval? { get }
14+
15+
func set(preferredFramesPerSecond: Int) -> Void
16+
17+
@available(iOS 15, macOS 14, tvOS 15, visionOS 1, *)
18+
func set(preferredFrameRateRange: CAFrameRateRange) -> Void
19+
20+
func start()
21+
func stop()
22+
}
23+
24+
#if !os(macOS)
25+
class RiveCADisplayLink: RiveDisplayLink {
26+
typealias Tick = () -> Void
27+
28+
private lazy var displayLink: CADisplayLink = {
29+
return CADisplayLink(target: self, selector: #selector(_tick))
30+
}()
31+
32+
var isPaused: Bool {
33+
get { displayLink.isPaused }
34+
set { displayLink.isPaused = newValue }
35+
}
36+
37+
var targetTimestamp: TimeInterval? {
38+
displayLink.targetTimestamp
39+
}
40+
41+
private let tick: Tick
42+
private var isActive = false
43+
44+
init(tick: @escaping () -> Void) {
45+
self.tick = tick
46+
}
47+
48+
deinit {
49+
displayLink.invalidate()
50+
}
51+
52+
func set(preferredFramesPerSecond fps: Int) {
53+
if #available(iOS 15, tvOS 15, visionOS 1, *) {
54+
let range = CAFrameRateRange(minimum: Float(fps), maximum: Float(fps), preferred: Float(fps))
55+
displayLink.preferredFrameRateRange = range
56+
} else {
57+
displayLink.preferredFramesPerSecond = fps
58+
}
59+
}
60+
61+
@available(iOS 15, tvOS 15, visionOS 1, *)
62+
func set(preferredFrameRateRange range: CAFrameRateRange) {
63+
displayLink.preferredFrameRateRange = range
64+
}
65+
66+
func start() {
67+
guard isActive == false else { return }
68+
displayLink.add(to: .main, forMode: .common)
69+
isActive = true
70+
}
71+
72+
func stop() {
73+
guard isActive == true else { return }
74+
displayLink.invalidate()
75+
isActive = false
76+
}
77+
78+
@objc private func _tick() {
79+
tick()
80+
}
81+
}
82+
#else
83+
@available(macOS 14, *)
84+
class RiveCADisplayLink: RiveDisplayLink {
85+
typealias Tick = () -> Void
86+
87+
private let view: NSView
88+
89+
private lazy var displayLink: CADisplayLink = {
90+
return view.displayLink(target: self, selector: #selector(_tick))
91+
}()
92+
93+
var isPaused: Bool {
94+
get { displayLink.isPaused }
95+
set { displayLink.isPaused = newValue }
96+
}
97+
98+
var targetTimestamp: TimeInterval? {
99+
displayLink.targetTimestamp
100+
}
101+
102+
private let tick: Tick
103+
private var isActive = false
104+
105+
init(view: NSView, tick: @escaping Tick) {
106+
self.view = view
107+
self.tick = tick
108+
}
109+
110+
deinit {
111+
displayLink.invalidate()
112+
}
113+
114+
func set(preferredFramesPerSecond fps: Int) {
115+
let range = CAFrameRateRange(minimum: Float(fps), maximum: Float(fps), preferred: Float(fps))
116+
displayLink.preferredFrameRateRange = range
117+
}
118+
119+
func set(preferredFrameRateRange range: CAFrameRateRange) {
120+
displayLink.preferredFrameRateRange = range
121+
}
122+
123+
func start() {
124+
guard isActive == false else { return }
125+
displayLink.add(to: .main, forMode: .common)
126+
isActive = true
127+
}
128+
129+
func stop() {
130+
guard isActive == true else { return }
131+
displayLink.invalidate()
132+
isActive = false
133+
}
134+
135+
@objc private func _tick() {
136+
tick()
137+
}
138+
}
139+
140+
class RiveCVDisplaySync: RiveDisplayLink {
141+
typealias Tick = () -> Void
142+
143+
private let tick: Tick
144+
145+
private var displayLink: CVDisplayLink?
146+
147+
var isPaused: Bool = false
148+
149+
let targetTimestamp: TimeInterval? = nil
150+
151+
init(tick: @escaping Tick) {
152+
self.tick = tick
153+
}
154+
155+
func set(preferredFramesPerSecond: Int) { }
156+
157+
func set(preferredFrameRateRange: CAFrameRateRange) { }
158+
159+
func start() {
160+
guard displayLink == nil else { return }
161+
162+
let result = CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
163+
guard result == kCVReturnSuccess, let displayLink else { return }
164+
165+
CVDisplayLinkSetOutputHandler(displayLink) { _, _, _, _, _ in
166+
DispatchQueue.main.async { [weak self] in
167+
self?.tick()
168+
}
169+
return kCVReturnSuccess
170+
}
171+
CVDisplayLinkStart(displayLink)
172+
}
173+
174+
func stop() {
175+
guard let displayLink else { return }
176+
CVDisplayLinkStop(displayLink)
177+
self.displayLink = nil
178+
}
179+
}
180+
#endif

0 commit comments

Comments
 (0)