fix: metal renderer teardown and resume

This commit is contained in:
Acaki committed 2025-07-15 18:08:54 +08:00
1 parent 8126173128
commit 686418997b
9 files changed
+216 -58

No files matched your search

+2
View File
@@ -19,5 +19,7 @@
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
- (void)drawableResize:(CGSize)drawableSize;
- (void)plotFrametime:(CFTimeInterval)presentedTime withPresentTime:(CFTimeInterval)presentTime;
- (void)stop;
- (void)discardNextDrawable;
@end
+36 -30
View File
@@ -95,8 +95,6 @@ static const NSUInteger MaxFramesInFlight = 3;
id<MTLBuffer> _VideoVertexBuffer;
CFTimeInterval _lastPresented;
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
dispatch_semaphore_t _inFlightSemaphore;
}
- (instancetype)initWithMetalDevice:(id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate {
@@ -114,7 +112,6 @@ static const NSUInteger MaxFramesInFlight = 3;
_lastColorSpace = -1;
_lastFullRange = NO;
_lastPresented = 0;
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
CFStringRef keys[1] = {kCVMetalTextureUsage};
NSUInteger values[1] = {MTLTextureUsageShaderRead};
@@ -131,10 +128,21 @@ static const NSUInteger MaxFramesInFlight = 3;
}
- (void)dealloc {
if (_CscParamsBuffer) {
_CscParamsBuffer = nil;
// Release the Core Foundation texture cache, which is not managed by ARC.
if (_textureCache) {
CFRelease(_textureCache);
_textureCache = NULL;
}
// Safely release any textures that might still be referenced.
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
if (_cvMetalTextures[i]) {
CFRelease(_cvMetalTextures[i]);
_cvMetalTextures[i] = NULL;
}
}
// ARC will handle the rest of the Objective-C objects like _CscParamsBuffer.
}
#if !TARGET_OS_TV
@@ -453,38 +461,21 @@ static const NSUInteger MaxFramesInFlight = 3;
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];
[renderEncoder setRenderPipelineState:_videoPipelineState];
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
for (size_t i = 0; i < planes; i++) {
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
}
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
dispatch_semaphore_signal(block_semaphore);
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
const double alpha = 0.25f;
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
// Free textures after completion of rendering
for (size_t i = 0; i < planes; i++) {
if (self->_cvMetalTextures[i]) {
CVBufferRelease(self->_cvMetalTextures[i]);
self->_cvMetalTextures[i] = nil;
}
}
CVMetalTextureCacheFlush(self->_textureCache, 0);
}];
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
[renderEncoder endEncoding];
__weak typeof(self) self_ = self;
__weak typeof(self) weakSelf = self;
[_nextDrawable addPresentedHandler:^(id<MTLDrawable> d) {
if (self_) {
[self_ plotFrametime:d.presentedTime];
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf plotFrametime:d.presentedTime];
}
}];
@@ -499,6 +490,19 @@ static const NSUInteger MaxFramesInFlight = 3;
// Wait for the command buffer to complete and free our CVMetalTextureCache references
[commandBuffer waitUntilCompleted];
// Perform cleanup
const CFTimeInterval GPUTime = commandBuffer.GPUEndTime - commandBuffer.GPUStartTime;
const double alpha = 0.25f;
_averageGPUTime = (GPUTime * alpha) + (_averageGPUTime * (1.0 - alpha));
for (size_t i = 0; i < planes; i++) {
if (_cvMetalTextures[i]) {
CVBufferRelease(_cvMetalTextures[i]);
_cvMetalTextures[i] = nil;
}
}
CVMetalTextureCacheFlush(_textureCache, 0);
_nextDrawable = nil;
} }
@@ -522,7 +526,6 @@ static const NSUInteger MaxFramesInFlight = 3;
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);
}
}
@@ -535,4 +538,7 @@ static const NSUInteger MaxFramesInFlight = 3;
}
- (void)stop {
}
@end
+4
View File
@@ -33,4 +33,8 @@
- (void)resizeDrawable:(CGFloat)scaleFactor;
#endif
- (void)stop;
- (void)pause;
- (void)resume;
@end
+58 -27
View File
@@ -11,6 +11,7 @@
// The flag to indicate that rendering needs to cease on the main thread.
BOOL _continueRunLoop;
dispatch_semaphore_t _renderThreadSemaphore;
}
#pragma mark - Initialization and Setup.
@@ -41,6 +42,7 @@
_metalLayer = (CAMetalLayer *)self.layer;
self.layer.delegate = self;
_renderThreadSemaphore = dispatch_semaphore_create(0);
}
#if TARGET_OS_IOS || TARGET_OS_TV
@@ -61,37 +63,44 @@
}
#endif // END TARGET_OS_IOS || TARGET_OS_TV
- (void)movedToWindow {
// Protect _continueRunLoop with a `@synchronized` block because it's accessed by the separate
// animation thread.
- (void)startRenderThread {
@synchronized(self) {
// Stop the animation loop, allowing it to complete if it's in progress.
_continueRunLoop = NO;
// Don't start a new thread if one is already running
if (_renderThread) {
return;
}
_continueRunLoop = YES;
_renderThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_renderThread start];
}
}
// Create and start a secondary NSThread that has another runloop. The NSThread
// class calls the 'runThread' method at the start of the secondary thread's execution.
_renderThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
_continueRunLoop = YES;
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_renderThread start];
- (void)movedToWindow {
if (self.window) {
// The view was added to a window, so start rendering.
[self resume];
// Notify the delegate of the drawable's size.
CGSize defaultDrawableSize = self.bounds.size;
defaultDrawableSize.width *= self.layer.contentsScale;
defaultDrawableSize.height *= self.layer.contentsScale;
[self.delegate drawableResize:defaultDrawableSize];
} else {
// The view was removed from a window, so stop rendering.
[self pause];
}
}
// 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
// components of the drawable's size.
#if AUTOMATICALLY_RESIZE
#if TARGET_OS_IOS || TARGET_OS_TV
[self resizeDrawable:self.window.screen.nativeScale];
#else
[self resizeDrawable:self.window.screen.backingScaleFactor];
#endif
#else
// Notify the delegate of the default drawable size when the system can calculate it.
CGSize defaultDrawableSize = self.bounds.size;
defaultDrawableSize.width *= self.layer.contentsScale;
defaultDrawableSize.height *= self.layer.contentsScale;
[self.delegate drawableResize:defaultDrawableSize];
#endif
- (void)pause {
// Pausing is implemented by simply stopping the render thread.
[self stop];
}
- (void)resume {
// Resuming is implemented by starting a new render thread.
[self startRenderThread];
}
- (void)runThread {
@@ -118,6 +127,7 @@
}
}
}
dispatch_semaphore_signal(self->_renderThreadSemaphore);
}
#pragma mark - Resizing
@@ -188,4 +198,25 @@
}
#endif // END AUTOMATICALLY_RESIZE
- (void)stop {
@synchronized(self) {
// If already stopping, do nothing.
if (!_continueRunLoop) {
return;
}
_continueRunLoop = NO;
}
if (_renderThread && _renderThread != [NSThread currentThread]) {
// Wait for the render thread to finish its loop and signal the semaphore.
// We use a 1-second timeout to prevent the app from hanging indefinitely
// if the thread gets stuck for some reason.
long timeoutResult = dispatch_semaphore_wait(_renderThreadSemaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (timeoutResult != 0) {
Log(LOG_E, @"MetalView render thread failed to stop gracefully within 1 second.");
}
}
_renderThread = nil;
}
@end
+4
View File
@@ -21,4 +21,8 @@
enableHdr:(BOOL)enableHdr
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
- (void)stop;
- (void)pause;
- (void)resume;
@end
+51 -1
View File
@@ -31,6 +31,7 @@ The implementation of the cross-platform game view controller.
_enableHdr = enableHdr;
_metricsHandler = metricsHandler;
_stopping = NO;
[_frameQueue clear];
}
return self;
}
@@ -86,7 +87,9 @@ The implementation of the cross-platform game view controller.
[_renderer waitToRenderTo:layer];
// If we don't have a frame yet, wait on that too
[_frameQueue waitForEnqueue];
if (!_stopping) {
[_frameQueue waitForEnqueue];
}
}
}
@@ -137,4 +140,51 @@ The implementation of the cross-platform game view controller.
}
#endif
- (void)stop {
// 1. Signal that we are stopping to prevent new work from starting.
_stopping = YES;
// 2. Invalidate the CAMetalLayer by removing its device.
// This should cause any blocking calls like `nextDrawable` on the render thread to fail
// and return immediately, breaking the deadlock.
if (_metalView) {
_metalView.metalLayer.device = nil;
}
// 3. Unblock the render thread from any other potential waiting points.
if (_renderer) {
[_renderer stop];
}
[_frameQueue stop];
// 4. Now that the thread is unblocked, wait for it to finish its execution.
if (_metalView) {
[_metalView stop];
}
// 5. Once the thread has terminated, it's safe to deallocate all resources.
_renderer = nil;
_metalView = nil;
}
- (void)pause {
if (_renderer) {
// Add this line to discard any stale drawable before pausing the thread.
// This forces the renderer to get a fresh one on resume.
[_renderer discardNextDrawable];
}
if (_metalView) {
[_metalView pause];
Log(LOG_I, @"Metal rendering paused.");
}
}
- (void)resume {
if (_metalView) {
[_metalView resume];
Log(LOG_I, @"Metal rendering resumed.");
}
}
@end