feat: add proper CADisplayLink handling for iOS15 where commandBuffer presentDrawable:drawable afterMinimumDuration doesn't seem to work well
This commit is contained in:
1 parent
ea1a2f3cd0
commit
ee67ff56c7
5 files changed
+126
-9
No files matched your search
@@ -22,12 +22,21 @@
|
|||||||
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
||||||
@property (atomic) BOOL isStopping;
|
@property (atomic) BOOL isStopping;
|
||||||
|
|
||||||
|
// iOS 15.0 and below frame timing properties
|
||||||
|
@property (nonatomic) BOOL useLegacyFrameTiming;
|
||||||
|
@property (nonatomic) CFTimeInterval lastDisplayLinkTime;
|
||||||
|
@property (nonatomic) CFTimeInterval displayLinkFrametime;
|
||||||
|
|
||||||
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate;
|
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate;
|
||||||
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
||||||
- (void)drawableResize:(CGSize)drawableSize;
|
- (void)drawableResize:(CGSize)drawableSize;
|
||||||
- (void)shutdown;
|
- (void)shutdown;
|
||||||
|
|
||||||
|
// iOS 15.0 and below legacy timing support
|
||||||
|
- (void)updateLegacyFrameTiming:(CFTimeInterval)displayLinkTimestamp;
|
||||||
|
- (void)resetFrameTiming;
|
||||||
|
|
||||||
+ (NSString *_Nullable)currentColorSpace;
|
+ (NSString *_Nullable)currentColorSpace;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -157,6 +157,17 @@ CFStringRef __currentColorSpace;
|
|||||||
_lastPresented = 0.0f;
|
_lastPresented = 0.0f;
|
||||||
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
||||||
_isStopping = NO;
|
_isStopping = NO;
|
||||||
|
|
||||||
|
// Enable legacy frame timing for iOS 15.0 and below to fix timing issues
|
||||||
|
if (@available(iOS 16.0, *)) {
|
||||||
|
_useLegacyFrameTiming = NO;
|
||||||
|
Log(LOG_I, @"MetalVideoRenderer: Using iOS 16+ timing (Metal presentedTime)");
|
||||||
|
} else {
|
||||||
|
_useLegacyFrameTiming = YES;
|
||||||
|
_lastDisplayLinkTime = 0.0f;
|
||||||
|
_displayLinkFrametime = 1.0f / framerate * 1000.0f; // Expected frametime in ms
|
||||||
|
Log(LOG_I, @"MetalVideoRenderer: Using legacy timing for iOS 15.0 and below (CADisplayLink)");
|
||||||
|
}
|
||||||
|
|
||||||
CFStringRef keys[1] = {kCVMetalTextureUsage};
|
CFStringRef keys[1] = {kCVMetalTextureUsage};
|
||||||
NSUInteger values[1] = {MTLTextureUsageShaderRead};
|
NSUInteger values[1] = {MTLTextureUsageShaderRead};
|
||||||
@@ -713,14 +724,21 @@ CFStringRef __currentColorSpace;
|
|||||||
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
||||||
[renderEncoder endEncoding];
|
[renderEncoder endEncoding];
|
||||||
|
|
||||||
__block MetalVideoRenderer *strongSelf = self;
|
|
||||||
#if !TARGET_OS_SIMULATOR
|
#if !TARGET_OS_SIMULATOR
|
||||||
|
__block MetalVideoRenderer *strongSelf = self;
|
||||||
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
|
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
|
||||||
if (strongSelf.lastPresented > 0.0f) {
|
// Only use Metal's presentedTime on iOS 16+ due to timing bugs on iOS 15.0
|
||||||
CFTimeInterval frametime = d.presentedTime - strongSelf.lastPresented;
|
if (!strongSelf.useLegacyFrameTiming) {
|
||||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
if (strongSelf.lastPresented > 0.0f) {
|
||||||
|
CFTimeInterval frametime = d.presentedTime - strongSelf.lastPresented;
|
||||||
|
// Sanity check: reject negative or impossibly large frametimes
|
||||||
|
if (frametime > 0 && frametime < 0.1) { // Less than 100ms
|
||||||
|
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strongSelf.lastPresented = d.presentedTime;
|
||||||
}
|
}
|
||||||
strongSelf.lastPresented = d.presentedTime;
|
// For iOS 15.0 and below, frametime is calculated in updateLegacyFrameTiming
|
||||||
}];
|
}];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -777,6 +795,43 @@ CFStringRef __currentColorSpace;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - iOS 15.0 and below legacy timing support
|
||||||
|
|
||||||
|
- (void)updateLegacyFrameTiming:(CFTimeInterval)displayLinkTimestamp {
|
||||||
|
if (!_useLegacyFrameTiming) {
|
||||||
|
return; // Only for iOS 15.0 and below
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastDisplayLinkTime > 0.0f) {
|
||||||
|
CFTimeInterval actualFrametime = (displayLinkTimestamp - _lastDisplayLinkTime) * 1000.0; // Convert to ms
|
||||||
|
|
||||||
|
// Sanity check: reject negative or impossibly large frametimes
|
||||||
|
if (actualFrametime > 0 && actualFrametime < 100.0) { // Less than 100ms
|
||||||
|
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:actualFrametime];
|
||||||
|
_displayLinkFrametime = actualFrametime;
|
||||||
|
} else {
|
||||||
|
// Use expected frametime if actual is unreliable
|
||||||
|
Log(LOG_W, @"MetalVideoRenderer: Rejecting invalid frametime %.2f ms, using expected %.2f ms", actualFrametime, _displayLinkFrametime);
|
||||||
|
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:_displayLinkFrametime];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastDisplayLinkTime = displayLinkTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)resetFrameTiming {
|
||||||
|
if (_useLegacyFrameTiming) {
|
||||||
|
// Reset legacy timing state for iOS 15.0 and below
|
||||||
|
_lastDisplayLinkTime = 0.0f;
|
||||||
|
_displayLinkFrametime = 1.0f / _framerate * 1000.0f;
|
||||||
|
Log(LOG_I, @"MetalVideoRenderer: Reset legacy frame timing for iOS 15.0 and below");
|
||||||
|
} else {
|
||||||
|
// Reset Metal timing state for iOS 16+
|
||||||
|
_lastPresented = 0.0f;
|
||||||
|
Log(LOG_I, @"MetalVideoRenderer: Reset Metal frame timing for iOS 16+");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Responds to the drawable's size or orientation changes.
|
/// Responds to the drawable's size or orientation changes.
|
||||||
- (void)drawableResize:(CGSize)drawableSize {
|
- (void)drawableResize:(CGSize)drawableSize {
|
||||||
[self resize:drawableSize];
|
[self resize:drawableSize];
|
||||||
|
|||||||
@@ -22,4 +22,7 @@
|
|||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||||
|
|
||||||
|
// Reset frame timing for iOS 15.0 and below compatibility
|
||||||
|
- (void)resetFrameTiming;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -92,19 +92,35 @@
|
|||||||
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
||||||
view.metalLayer.maximumDrawableCount = 3;
|
view.metalLayer.maximumDrawableCount = 3;
|
||||||
|
|
||||||
// We need a no-op displaylink timer or iOS can decide to run at 60fps
|
// We need a displaylink timer for iOS 15.0 and below to provide accurate frame timing,
|
||||||
// The overhead from this should be minimal.
|
// or just as a no-op for iOS 16+ to prevent iOS from dropping to 60fps
|
||||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkHandler:)];
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkHandler:)];
|
||||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
if (@available(iOS 16.0, tvOS 16.0, *)) {
|
||||||
|
// iOS 16+: Use CAFrameRateRange, displaylink is just a no-op
|
||||||
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
|
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
|
||||||
|
Log(LOG_I, @"MetalViewController: DisplayLink using iOS 16+ CAFrameRateRange");
|
||||||
|
} else if (@available(iOS 15.0, tvOS 15.0, *)) {
|
||||||
|
// iOS 15.0: Use legacy preferredFramesPerSecond to avoid CAFrameRateRange bugs
|
||||||
|
_displayLink.preferredFramesPerSecond = _framerate;
|
||||||
|
Log(LOG_I, @"MetalViewController: DisplayLink using iOS 15.0 legacy preferredFramesPerSecond");
|
||||||
} else {
|
} else {
|
||||||
|
// iOS 14 and below
|
||||||
_displayLink.preferredFramesPerSecond = _framerate;
|
_displayLink.preferredFramesPerSecond = _framerate;
|
||||||
}
|
}
|
||||||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)displayLinkHandler:(CADisplayLink *)link {
|
- (void)displayLinkHandler:(CADisplayLink *)link {
|
||||||
// Rendering does not use DisplayLink, this exists to fool iOS into keeping us running at the desired framerate
|
// For iOS 16+: This is just a no-op to fool iOS into keeping us running at the desired framerate
|
||||||
|
// For iOS 15.0 and below: Update renderer with accurate frame timing to fix negative framerates
|
||||||
|
if (@available(iOS 16.0, *)) {
|
||||||
|
// No-op for iOS 16+, Metal's presentedTime is reliable
|
||||||
|
} else {
|
||||||
|
// iOS 15.0 and below: Use DisplayLink timing since Metal's presentedTime is unreliable
|
||||||
|
if (_renderer) {
|
||||||
|
[_renderer updateLegacyFrameTiming:link.timestamp];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||||
@@ -154,4 +170,32 @@
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
- (void)resetFrameTiming {
|
||||||
|
// Reset the renderer's frame timing state for iOS 15.0 and below compatibility
|
||||||
|
if (_renderer) {
|
||||||
|
[_renderer resetFrameTiming];
|
||||||
|
Log(LOG_I, @"MetalViewController: Reset frame timing called");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset DisplayLink timing for iOS 15.0 and below
|
||||||
|
if (@available(iOS 16.0, *)) {
|
||||||
|
// No additional DisplayLink reset needed for iOS 16+
|
||||||
|
} else {
|
||||||
|
// For iOS 15.0 and below, pause and resume DisplayLink to re-engage timing
|
||||||
|
if (_displayLink) {
|
||||||
|
_displayLink.paused = YES;
|
||||||
|
|
||||||
|
// Re-apply frame rate (avoid CAFrameRateRange on iOS 15.0)
|
||||||
|
if (@available(iOS 15.0, *)) {
|
||||||
|
_displayLink.preferredFramesPerSecond = _framerate;
|
||||||
|
} else {
|
||||||
|
_displayLink.preferredFramesPerSecond = _framerate;
|
||||||
|
}
|
||||||
|
|
||||||
|
_displayLink.paused = NO;
|
||||||
|
Log(LOG_I, @"MetalViewController: Reset DisplayLink timing for iOS 15.0 and below");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -1043,6 +1043,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[self->_streamMan.videoRenderer resetFramePacing];
|
[self->_streamMan.videoRenderer resetFramePacing];
|
||||||
|
|
||||||
|
// Also reset Metal renderer timing for iOS 15.0 and below compatibility
|
||||||
|
if (self.metalViewController) {
|
||||||
|
[self.metalViewController resetFrameTiming];
|
||||||
|
}
|
||||||
|
|
||||||
_isRestoringFromPiP = NO;
|
_isRestoringFromPiP = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user