From 112c74c1b9a3bb70079c44cca77776c425145dfc Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 29 Nov 2023 17:05:57 +0800 Subject: [PATCH] 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 --- SDWebImage/Private/SDDisplayLink.m | 46 ++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/SDWebImage/Private/SDDisplayLink.m b/SDWebImage/Private/SDDisplayLink.m index 36704fd0..43d9ffb8 100644 --- a/SDWebImage/Private/SDDisplayLink.m +++ b/SDWebImage/Private/SDDisplayLink.m @@ -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 { + // Invalid, fallback `duration` + duration = self.displayLink.duration; + } } else { - // iOS 9 use `previousTime` - duration = CACurrentMediaTime() - self.previousFireTime; + // 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