From 112c74c1b9a3bb70079c44cca77776c425145dfc Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 29 Nov 2023 17:05:57 +0800 Subject: [PATCH 1/4] 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 From 40b3d7f4385b0825b456a90dd24c115679e33c23 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 29 Nov 2023 17:37:29 +0800 Subject: [PATCH 2/4] Remove the unused legacy code for weak retain --- SDWebImage/Private/SDDisplayLink.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SDWebImage/Private/SDDisplayLink.m b/SDWebImage/Private/SDDisplayLink.m index 43d9ffb8..0472a2f7 100644 --- a/SDWebImage/Private/SDDisplayLink.m +++ b/SDWebImage/Private/SDDisplayLink.m @@ -272,10 +272,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt return kCVReturnSuccess; } CVTimeStamp outputTime = inOutputTime ? *inOutputTime : *inNow; - __weak SDDisplayLink *weakObject = object; + // `SDWeakProxy` is weak, so it's safe to dispatch to main queue without leak dispatch_async(dispatch_get_main_queue(), ^{ - weakObject.outputTime = outputTime; - [weakObject displayLinkDidRefresh:(__bridge id)(displayLink)]; + object.outputTime = outputTime; + [object displayLinkDidRefresh:(__bridge id)(displayLink)]; }); return kCVReturnSuccess; } From 5dec4049c1ce6ea3c1c2db13383e3e52150d7273 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 29 Nov 2023 17:46:43 +0800 Subject: [PATCH 3/4] Update the new test case for `testSDDisplayLink` --- Tests/Tests/SDUtilsTests.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index 09c1ff22..a1cde73e 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -16,6 +16,8 @@ @interface SDUtilsTests : SDTestCase +@property (nonatomic) NSTimeInterval duration; + @end @implementation SDUtilsTests @@ -53,6 +55,8 @@ expect(displayLink.isRunning).beTruthy(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ expect(displayLink.isRunning).beTruthy(); + NSTimeInterval duration = self.duration; // Should be 1, 200ms accuracy + expect(duration).beCloseToWithin(1, 0.2); [displayLink stop]; }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ @@ -70,8 +74,7 @@ - (void)displayLinkDidRefresh:(SDDisplayLink *)displayLink { NSTimeInterval duration = displayLink.duration; // Running value - expect(duration).beGreaterThan(0.001); /// 60Hz ~ 120Hz - expect(duration).beLessThan(0.02); + self.duration += duration; } - (void)testSDFileAttributeHelper { From e4243aa13b525af2aa7e5c61b20d1e6f5103e475 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 29 Nov 2023 17:52:23 +0800 Subject: [PATCH 4/4] Ignore the availability warning --- SDWebImage/Private/SDDisplayLink.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SDWebImage/Private/SDDisplayLink.m b/SDWebImage/Private/SDDisplayLink.m index 0472a2f7..f595f79a 100644 --- a/SDWebImage/Private/SDDisplayLink.m +++ b/SDWebImage/Private/SDDisplayLink.m @@ -17,7 +17,9 @@ #if SD_MAC static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext); -#else +#endif + +#if SD_UIKIT static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or `previous` fire time (only for CADisplayLink) #endif @@ -91,6 +93,8 @@ static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or ` return displayLink; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" - (NSTimeInterval)duration { NSTimeInterval duration = 0; #if SD_MAC @@ -155,6 +159,7 @@ static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or ` } return duration; } +#pragma clang diagnostic pop - (BOOL)isRunning { #if SD_MAC @@ -240,6 +245,8 @@ static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or ` self.nextFireTime = 0; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" - (void)displayLinkDidRefresh:(id)displayLink { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" @@ -256,6 +263,7 @@ static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or ` self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink); #endif } +#pragma clang diagnostic pop @end