Use the new solution for CADisplayLink duration calculation based on WWDC 10147

This visually fix visionOS (90Hz) animated image duration

We don't use Media Time because it's not correct when lag or VSync not callback in current runloop

Instead, we use the `next targetTimestamp - previous targetTimestamp` to get the actual time during callbacks
This commit is contained in:
DreamPiggy 2023-11-29 17:05:57 +08:00
parent 1b9a2e902c
commit 112c74c1b9
1 changed files with 34 additions and 12 deletions

View File

@ -17,6 +17,8 @@
#if SD_MAC
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext);
#else
static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or `previous` fire time (only for CADisplayLink)
#endif
#define kSDDisplayLinkInterval 1.0 / 60
@ -65,6 +67,12 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
_selector = sel;
// CA/CV/NSTimer will retain to the target, we need to break this using weak proxy
SDWeakProxy *weakProxy = [SDWeakProxy proxyWithTarget:self];
#if SD_UIKIT
if (@available(iOS 10.0, tvOS 10.0, *)) {
// Use static bool, which is a little faster than runtime OS version check
kSDDisplayLinkUseTargetTimestamp = YES;
}
#endif
#if SD_MAC
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
// Simulate retain for target, the target is weak proxy to self
@ -92,18 +100,32 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
duration = (double)outputTime.videoRefreshPeriod / periodPerSecond;
}
#elif SD_UIKIT
// iOS 10+/watchOS use `nextTime`
if (@available(iOS 10.0, tvOS 10.0, *)) {
duration = self.nextFireTime - CACurrentMediaTime();
// iOS 10+ use current `targetTimestamp` - previous `targetTimestamp`
// See: WWDC Session 10147 - Optimize for variable refresh rate displays
if (kSDDisplayLinkUseTargetTimestamp) {
NSTimeInterval nextFireTime = self.nextFireTime;
if (nextFireTime != 0) {
duration = self.displayLink.targetTimestamp - nextFireTime;
} else {
// iOS 9 use `previousTime`
duration = CACurrentMediaTime() - self.previousFireTime;
// Invalid, fallback `duration`
duration = self.displayLink.duration;
}
} else {
// iOS 9 use current `timestamp` - previous `timestamp`
NSTimeInterval previousFireTime = self.previousFireTime;
if (previousFireTime != 0) {
duration = self.displayLink.timestamp - previousFireTime;
} else {
// Invalid, fallback `duration`
duration = self.displayLink.duration;
}
}
#else
if (self.nextFireTime != 0) {
NSTimeInterval nextFireTime = self.nextFireTime;
if (nextFireTime != 0) {
// `CFRunLoopTimerGetNextFireDate`: This time could be a date in the past if a run loop has not been able to process the timer since the firing time arrived.
// Don't rely on this, always calculate based on elapsed time
duration = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink) - self.nextFireTime;
duration = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink) - nextFireTime;
}
#endif
// When system sleep, the targetTimestamp will mass up, fallback refresh rate
@ -219,17 +241,17 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
}
- (void)displayLinkDidRefresh:(id)displayLink {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[_target performSelector:_selector withObject:self];
#pragma clang diagnostic pop
#if SD_UIKIT
if (@available(iOS 10.0, tvOS 10.0, *)) {
if (kSDDisplayLinkUseTargetTimestamp) {
self.nextFireTime = self.displayLink.targetTimestamp;
} else {
self.previousFireTime = self.displayLink.timestamp;
}
#endif
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[_target performSelector:_selector withObject:self];
#pragma clang diagnostic pop
#if SD_WATCH
self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink);
#endif