diff --git a/VoidLink/ImGuiRenderer.mm b/VoidLink/ImGuiRenderer.mm index a819a163..a1eba0a2 100644 --- a/VoidLink/ImGuiRenderer.mm +++ b/VoidLink/ImGuiRenderer.mm @@ -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; diff --git a/VoidLink/Metal/MetalViewController.h b/VoidLink/Metal/MetalViewController.h index 29e51cdc..870e2b37 100644 --- a/VoidLink/Metal/MetalViewController.h +++ b/VoidLink/Metal/MetalViewController.h @@ -22,4 +22,7 @@ enableHdr:(BOOL)enableHdr metricsHandler:(MetricsHandler _Nonnull)metricsHandler; +- (void)pauseRendering; +- (void)resumeRendering; + @end diff --git a/VoidLink/Metal/MetalViewController.m b/VoidLink/Metal/MetalViewController.m index 9c5395da..46868bcd 100644 --- a/VoidLink/Metal/MetalViewController.m +++ b/VoidLink/Metal/MetalViewController.m @@ -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]; diff --git a/VoidLink/Stream/VideoDecoderRenderer.m b/VoidLink/Stream/VideoDecoderRenderer.m index fb08abac..cc9e24e0 100644 --- a/VoidLink/Stream/VideoDecoderRenderer.m +++ b/VoidLink/Stream/VideoDecoderRenderer.m @@ -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]; }); }); diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index d37f8266..7479311f 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -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) {