Revert "feat: use CAMetalDisplayLink for metal renderer to fix refresh rate issues"

This reverts commit 628ce0f12e.
This commit is contained in:
Acaki
2025-08-13 15:15:52 +08:00
parent 2ab705efdf
commit 868288e70f
9 changed files with 44 additions and 143 deletions
+1 -9
View File
@@ -52,15 +52,7 @@
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
// Set rendering backend based on iOS version and user preference
NSInteger savedBackend = [[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"];
if (@available(iOS 17.0, *)) {
// iOS 17+ can use Performance mode (Metal renderer)
self.renderingBackend = [NSNumber numberWithInteger:savedBackend];
} else {
// iOS < 17 must use Balanced mode (AVSB renderer)
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
}
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
-1
View File
@@ -28,7 +28,6 @@ typedef void (^MetricsHandler)(int plotId, CFTimeInterval value);
@property (nonatomic) BOOL enableGraphs;
@property (nonatomic) float graphOpacity;
@property (nonatomic) BOOL imguiRunning;
@property (nonatomic) int streamFps;
@property (nonatomic) MetricsHandler _Nonnull metricsHandler;
-(nonnull instancetype) initWithFrame:(CGRect)bounds
+2 -5
View File
@@ -42,9 +42,6 @@
_plots = [ImGuiPlots sharedInstance].plots;
// Store stream FPS to synchronize MTKView
_streamFps = streamFps;
return self;
}
@@ -93,7 +90,7 @@
self.mtkView.device = self.device;
self.mtkView.delegate = self;
self.mtkView.preferredFramesPerSecond = _streamFps; // Synchronize with stream framerate
self.mtkView.preferredFramesPerSecond = 60; // ImGui overlay will always render at this rate
self.mtkView.opaque = NO;
self.mtkView.enableSetNeedsDisplay = NO;
@@ -176,7 +173,7 @@
#if TARGET_OS_SIMULATOR
[commandBuffer presentDrawable:view.currentDrawable];
#else
[commandBuffer presentDrawable:view.currentDrawable afterMinimumDuration:1.0 / _streamFps];
[commandBuffer presentDrawable:view.currentDrawable afterMinimumDuration:1.0 / view.preferredFramesPerSecond];
#endif
[commandBuffer commit];
#endif
+1 -2
View File
@@ -23,8 +23,7 @@
@property (atomic) BOOL isStopping;
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate;
- (void)renderFrame:(nonnull Frame *)frame withDrawable:(nonnull id<CAMetalDrawable>)drawable API_AVAILABLE(ios(17.0));
- (void)renderFrame:(nonnull Frame *)frame withDrawable:(nonnull id<CAMetalDrawable>)drawable targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0));
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
- (void)drawableResize:(CGSize)drawableSize;
- (void)shutdown;
+26 -15
View File
@@ -459,30 +459,21 @@ CFStringRef __currentColorSpace;
return YES;
}
// Enhanced method that receives timing information from CAMetalDisplayLink
- (void)renderFrame:(Frame *)frame withDrawable:(id<CAMetalDrawable>)drawable targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0)) {
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
@autoreleasepool {
if (self.isStopping) {
Log(LOG_I, @"[MetalVideoRenderer] isStopping");
return;
}
// Calculate accurate frametime using CAMetalDisplayLink timing
if (self.lastPresented > 0.0f) {
CFTimeInterval frametime = targetPresentationTimestamp - self.lastPresented;
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
}
self.lastPresented = targetPresentationTimestamp;
// Handle changes to the frame's colorspace from last time we rendered
BOOL layerDidChange = NO;
if (![self updateColorSpaceForFrame:frame toLayer:(CAMetalLayer *)drawable.layer layerDidChange:&layerDidChange]) {
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
return;
}
// Handle changes to the video size or drawable size
if (![self updateVideoRegionSizeForFrame:frame toLayer:(CAMetalLayer *)drawable.layer]) {
if (![self updateVideoRegionSizeForFrame:frame toLayer:layer]) {
return;
}
@@ -505,6 +496,13 @@ CFStringRef __currentColorSpace;
}
}
// Get the next drawable early to get its pixel format
id<CAMetalDrawable> drawable = [layer nextDrawable];
if (!drawable) {
Log(LOG_E, @"Failed to get nextDrawable");
return;
}
// Get the framebuffer pixel format for pipeline creation
MTLPixelFormat framebufferPixelFormat = drawable.texture.pixelFormat;
@@ -598,7 +596,6 @@ CFStringRef __currentColorSpace;
}
}
// Use the provided drawable instead of getting nextDrawable from layer
_renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
@@ -612,7 +609,7 @@ CFStringRef __currentColorSpace;
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
#if !TARGET_OS_TV
if (((CAMetalLayer *)drawable.layer).pixelFormat == MTLPixelFormatRGBA16Float) {
if (layer.pixelFormat == MTLPixelFormatRGBA16Float) {
[self pollCurrentEDRHeadroom];
[renderEncoder setFragmentBytes:&_currentEDRHeadroom length:sizeof(float) atIndex:1];
}
@@ -620,6 +617,15 @@ CFStringRef __currentColorSpace;
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
[renderEncoder endEncoding];
__block MetalVideoRenderer *strongSelf = self;
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
if (strongSelf.lastPresented > 0.0f) {
CFTimeInterval frametime = d.presentedTime - strongSelf.lastPresented;
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
}
strongSelf.lastPresented = d.presentedTime;
}];
// signal semaphore, compute GPU time average, and clear textures
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
@@ -640,10 +646,15 @@ CFStringRef __currentColorSpace;
CVMetalTextureCacheFlush(self->_textureCache, 0);
}];
// CAMetalDisplayLink handles timing automatically, so we don't use afterMinimumDuration
#if TARGET_OS_SIMULATOR
[commandBuffer presentDrawable:drawable];
#else
// present for a minimum duration for best frame pacing
[commandBuffer presentDrawable:drawable afterMinimumDuration:1.0f / _framerate];
#endif
[commandBuffer commit];
[commandBuffer waitUntilCompleted];
}
}
+2 -3
View File
@@ -16,14 +16,13 @@
@protocol MetalViewDelegate <NSObject>
- (void)drawableResize:(CGSize)size;
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(17.0));
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0));
- (void)renderTo:(nonnull CAMetalLayer *)layer;
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
@end
// The Metal game view base class.
@interface MetalView : UIView <CALayerDelegate, CAMetalDisplayLinkDelegate>
@interface MetalView : UIView <CALayerDelegate>
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
+8 -82
View File
@@ -11,13 +11,10 @@
#import "MetalView.h"
#import "MetalConfig.h"
#import <QuartzCore/CAMetalDisplayLink.h>
@implementation MetalView {
// The secondary thread containing the render loop.
NSThread *_renderThread;
// Metal display link for vsync synchronization (iOS 17+)
CAMetalDisplayLink *_metalDisplayLink API_AVAILABLE(ios(17.0));
}
#pragma mark - Initialization and Setup.
@@ -43,43 +40,15 @@
self.layer.delegate = self;
}
- (void)setFramerate:(float)framerate {
_framerate = framerate;
if (@available(iOS 17.0, *)) {
if (_metalDisplayLink) {
_metalDisplayLink.preferredFrameRateRange = CAFrameRateRangeMake(framerate, framerate, framerate);
}
}
}
- (void)shutdown {
// First cancel the thread to stop the run loop
if (_renderThread) {
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
[_renderThread cancel];
// Invalidate metal display link to stop callbacks
if (@available(iOS 17.0, *)) {
if (_metalDisplayLink) {
Log(LOG_I, @"[MetalView] invalidating metal display link");
[_metalDisplayLink invalidate];
_metalDisplayLink = nil;
}
}
// Now wait for thread to finish
Log(LOG_I, @"[MetalView] waiting on renderThread to finish");
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2.0]; // 2 second timeout
while (!_renderThread.isFinished && [timeout timeIntervalSinceNow] > 0) {
usleep(1000);
}
if (_renderThread.isFinished) {
Log(LOG_I, @"[MetalView] renderThread has finished");
} else {
Log(LOG_E, @"[MetalView] renderThread failed to finish in time");
while (!_renderThread.isFinished) {
usleep(100);
}
Log(LOG_I, @"[MetalView] renderThread has finished");
_renderThread = nil;
}
}
@@ -92,28 +61,6 @@
[self movedToWindow];
}
// CAMetalDisplayLinkDelegate method (iOS 17+)
- (void)metalDisplayLink:(CAMetalDisplayLink *)link needsUpdate:(CAMetalDisplayLinkUpdate *)update API_AVAILABLE(ios(17.0)) {
// Skip rendering if we're shutting down
if ([NSThread currentThread].isCancelled) {
return;
}
@autoreleasepool {
[self.delegate waitToRenderTo:self.metalLayer];
// Get the drawable from the update object
id<CAMetalDrawable> drawable = update.drawable;
if (drawable) {
// Pass timing information to the renderer
if ([self.delegate respondsToSelector:@selector(renderWithDrawable:toLayer:targetPresentationTimestamp:)]) {
[self.delegate renderWithDrawable:drawable toLayer:self.metalLayer targetPresentationTimestamp:update.targetPresentationTimestamp];
} else {
[self.delegate renderWithDrawable:drawable toLayer:self.metalLayer];
}
}
}
}
- (void)movedToWindow {
if (!self.window) {
Log(LOG_I, @"[MetalView] movedToWindow(nil): shutting down...");
@@ -121,33 +68,12 @@
return;
}
// Create CAMetalDisplayLink for vsync-synchronized rendering (iOS 17+ only)
if (@available(iOS 17.0, *)) {
_metalDisplayLink = [[CAMetalDisplayLink alloc] initWithMetalLayer:_metalLayer];
_metalDisplayLink.delegate = self;
_metalDisplayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
Log(LOG_I, @"[MetalView] Using CAMetalDisplayLink for optimal Metal rendering");
}
// Start the display link on a background thread
// Render on a new thread
_renderThread = [[NSThread alloc] initWithBlock:^{
// Add metal display link to this thread's run loop
if (@available(iOS 17.0, *)) {
[self->_metalDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
// Keep the run loop alive, checking for cancellation regularly
while (![NSThread currentThread].isCancelled && self->_metalDisplayLink) {
while (![NSThread currentThread].isCancelled) {
@autoreleasepool {
// Run the run loop for a short time to allow checking cancellation
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
// Remove from run loop if still valid
if (@available(iOS 17.0, *)) {
if (self->_metalDisplayLink) {
[self->_metalDisplayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.delegate waitToRenderTo:self.metalLayer];
[self.delegate renderTo:self.metalLayer];
}
}
Log(LOG_I, @"[MetalView] renderThread is exiting");
@@ -155,7 +81,7 @@
_renderThread.name = @"MetalVideoRenderer";
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_renderThread start];
Log(LOG_I, @"[MetalView] started renderThread with CAMetalDisplayLink at %f fps", _framerate);
Log(LOG_I, @"[MetalView] started renderThread %@", _renderThread);
// Perform any actions that need to know the size and scale of the drawable. When UIKit calls
// didMoveToWindow after the view initialization, this is the first opportunity to notify
+3 -21
View File
@@ -82,25 +82,13 @@
[_frameQueue waitForEnqueue];
}
// New method for CAMetalDisplayLink that provides the drawable
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(17.0)) {
/// Draw frame (used by manual loop)
- (void)renderTo:(nonnull CAMetalLayer *)layer {
if (!_renderer.isStopping) {
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
if (frame) {
[_renderer renderFrame:frame withDrawable:drawable];
}
}
}
// Enhanced method that receives timing information from CAMetalDisplayLink
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0)) {
if (!_renderer.isStopping) {
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
if (frame) {
[_renderer renderFrame:frame withDrawable:drawable targetPresentationTimestamp:targetPresentationTimestamp];
[_renderer renderFrame:frame toLayer:layer];
}
}
}
@@ -114,14 +102,8 @@
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
// Shutdown the renderer first
[_renderer shutdown];
_renderer = nil;
// Then shutdown the view's display link
if (_metalView) {
[_metalView shutdown];
}
}
#if TARGET_OS_IOS
@@ -536,11 +536,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
// Only show Metal renderer option on iOS 17+ where CAMetalDisplayLink is available
if (@available(iOS 17.0, *)) {
[self addSetting:self.renderingBackendStack ofId:@"renderingBackendStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
}
[self addSetting:self.renderingBackendStack ofId:@"renderingBackendStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[videoSection addToParentStack:_parentStack];
[videoSection setExpanded:YES];