feat: pause Metal renderer and graph when app is in background

This commit is contained in:
Acaki
2025-08-25 00:42:01 +08:00
parent a2c4d440db
commit c8f61b0b87
5 changed files with 98 additions and 19 deletions
+6
View File
@@ -128,6 +128,12 @@
- (void)drawInMTKView:(MTKView *)view
{
#if !defined(IMGUI_DISABLE)
// Don't render if app is in background to avoid GPU submission errors
// Allow rendering when inactive (control center, notifications)
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) {
return;
}
ImGuiIO &io = ImGui::GetIO();
io.DisplaySize.x = view.bounds.size.width;
io.DisplaySize.y = view.bounds.size.height;
+3
View File
@@ -22,4 +22,7 @@
enableHdr:(BOOL)enableHdr
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
- (void)pauseRendering;
- (void)resumeRendering;
@end
+33 -2
View File
@@ -91,6 +91,11 @@
}
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
// Skip waiting when renderer is paused
if (_renderer.isStopping) {
return;
}
// Renderer obtains a nextDrawable, waiting if necessary
if (@available(iOS 13.0, *)) {
[_renderer waitToRenderTo:layer];
@@ -102,14 +107,20 @@
/// Draw frame (used by manual loop)
- (void)renderTo:(nonnull CAMetalLayer *)layer {
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
if (!_renderer.isStopping) {
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
// Only render if not paused
if (frame) {
if (@available(iOS 13.0, *)) {
[_renderer renderFrame:frame toLayer:layer];
}
}
} else {
// When paused, we still dequeue frames to prevent accumulation
// but don't render them. Also sleep a bit to reduce CPU usage
usleep(100000);
}
}
@@ -117,6 +128,26 @@
[_renderer drawableResize:size];
}
- (void)pauseRendering {
if (_displayLink) {
_displayLink.paused = YES;
}
if (_renderer) {
_renderer.isStopping = YES;
}
Log(LOG_I, @"[MetalViewController] Rendering paused");
}
- (void)resumeRendering {
if (_renderer) {
_renderer.isStopping = NO;
}
if (_displayLink) {
_displayLink.paused = NO;
}
Log(LOG_I, @"[MetalViewController] Rendering resumed");
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
+34 -17
View File
@@ -291,7 +291,9 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
// we missed a callback
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
}
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
}
}
lastTargetLocal = targetLocal;
@@ -843,11 +845,24 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
frameNumber:(int)frameNumber
frameType:(int)frameType
decodeStartTime:(CFTimeInterval)decodeStartTime {
if (frameType == FRAME_TYPE_IDR || _decompressionSession == nil) {
// Check if we need to create/recreate the decompression session
BOOL needsNewSession = (frameType == FRAME_TYPE_IDR || _decompressionSession == nil);
// Also check if the session might have been invalidated by iOS during background
if (!needsNewSession && _decompressionSession != nil) {
Boolean isValid = VTDecompressionSessionCanAcceptFormatDescription(_decompressionSession, _formatDesc);
if (!isValid) {
Log(LOG_W, @"Decompression session is invalid, needs recreation");
needsNewSession = YES;
}
}
if (needsNewSession) {
[self setupDecompressionSession];
}
if (_decompressionSession == nil) {
Log(LOG_E, @"Failed to create decompression session");
return kVTInvalidSessionErr;
}
@@ -910,24 +925,26 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
}
int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3];
static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
[[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped];
[[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped];
// It's important we capture host metrics on the incoming thread, as this frame object
// may have been dropped by the above enqueue
static CFTimeInterval lastHostFrame = 0.0f;
if (lastHostFrame != 0) {
[[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(frame.pts - lastHostFrame) * 1000.0];
// It's important we capture host metrics on the incoming thread, as this frame object
// may have been dropped by the above enqueue
static CFTimeInterval lastHostFrame = 0.0f;
if (lastHostFrame != 0) {
[[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(frame.pts - lastHostFrame) * 1000.0];
}
lastHostFrame = frame.pts;
// Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats
static PlotMetrics decodeMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE value:(CACurrentMediaTime() - decodeStartTime) * 1000.0 plotMetrics:&decodeMetrics];
[self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics];
}
lastHostFrame = frame.pts;
// Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats
static PlotMetrics decodeMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE value:(CACurrentMediaTime() - decodeStartTime) * 1000.0 plotMetrics:&decodeMetrics];
[self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics];
});
});
@@ -1042,6 +1042,17 @@
[self.pipController stopPictureInPicture];
}
if ([_settings.renderingBackend intValue] == RENDER_METAL && self.metalViewController) {
Log(LOG_I, @"Resuming Metal renderer on foreground");
[self.metalViewController resumeRendering];
}
if (self.imguiView && self.imguiView.mtkView && _settings.enableGraphs) {
self.imguiView.mtkView.enableSetNeedsDisplay = YES;
self.imguiView.mtkView.paused = NO;
Log(LOG_I, @"Resuming ImGui renderer on foreground");
}
[self->_streamMan.videoRenderer resetFramePacing];
_isRestoringFromPiP = NO;
}
@@ -1052,6 +1063,17 @@
NSLog(@"did enter background, %d, %@, %d", _settings.enablePIP, self.pipController, self.pipController.isPictureInPictureActive);
if (_settings.enablePIP && self.pipController && self.pipController.isPictureInPictureActive) {
//Log(LOG_I, @"PIP is active, not terminating stream");
} else {
if ([_settings.renderingBackend intValue] == RENDER_METAL && self.metalViewController) {
Log(LOG_I, @"Pausing Metal renderer on background");
[self.metalViewController pauseRendering];
}
if (self.imguiView && self.imguiView.mtkView) {
self.imguiView.mtkView.paused = YES;
self.imguiView.mtkView.enableSetNeedsDisplay = NO;
Log(LOG_I, @"Pausing ImGui renderer on background");
}
}
if (_inactivityTimer != nil) {