Allow switching rendering backend without restarting the app

The renderer setting previously forced an app quit (exit(0)) because
switching backends between sessions crashed or leaked. Fix the
underlying lifecycle bugs and remove the restart requirement:

- FrameQueue: really release frames in clear(), clear on session
  start/stop, and guard start/stop with an owner so a stale session's
  async cleanup can't pause a new session's queue
- MetalView: weak delegate (breaks retain cycle leaking the whole Metal
  stack), never start two render threads, bound the shutdown wait to
  avoid deadlocking against the render thread's dispatch_sync to main
- MetalViewController: explicit idempotent shutdown called from stream
  teardown (viewDidDisappear isn't guaranteed); snapshot the renderer
  in render-thread callbacks to avoid use-after-free during shutdown
- MetalVideoRenderer: make the shared colorspace name thread-safe and
  ARC-managed instead of a CFStringRef over-released in dealloc; guard
  NULL format description extensions
- Connection: drop the static renderer reference in DrCleanup
- VideoDecoderRenderer: cache the application background state via
  notifications instead of calling UIKit from the decode queue
  (Main Thread Checker violation)
- Frame: don't CFRetain(NULL) when a format desc has no extensions
- StreamFrameViewController: fix _streamView cleanup ordering
- Settings: apply the backend change in place (with the Metal caveats
  alert) and update localized tips; takes effect on next stream

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Acaki
2026-08-08 17:14:01 +08:00
co-authored by Claude Fable 5
parent fc2b1689d0
commit 6879bf6379
13 changed files with 344 additions and 172 deletions
+151 -105
View File
@@ -5090,6 +5090,29 @@
}
}
},
"Apply" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "应用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "套用"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "套用"
}
}
}
},
"Arrow direction pad" : {
"extractionState" : "manual",
"localizations" : {
@@ -8208,6 +8231,29 @@
}
}
},
"Enable Metal Renderer?" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "启用 Metal 渲染?"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "啟用 Metal 渲染?"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "啟用 Metal 渲染?"
}
}
}
},
"enableOswForNativeTouchSwitchTip" : {
"extractionState" : "manual",
"localizations" : {
@@ -9304,98 +9350,6 @@
}
}
},
"Interpolation failed for an unknown reason." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因导致的插帧失败"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
}
}
},
"Interpolation overhead is too high. Falling back to normal mode." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧性能开销过大,回退普通模式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
}
}
},
"Interpolator error:" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器错误:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
}
}
},
"Interpolator started." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器已启动。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
}
}
},
"framePacingStackTip" : {
"extractionState" : "manual",
"localizations" : {
@@ -10883,6 +10837,98 @@
}
}
},
"Interpolation failed for an unknown reason." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因导致的插帧失败"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知原因導致插幀失敗"
}
}
}
},
"Interpolation overhead is too high. Falling back to normal mode." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧性能开销过大,回退普通模式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀效能開銷過大,回退至一般模式"
}
}
}
},
"Interpolator error:" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器错误:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器錯誤:"
}
}
}
},
"Interpolator started." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "插帧器已启动。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "插幀器已啟動。"
}
}
}
},
"Interval of Multi-Touch Move Events: %d μs" : {
"extractionState" : "manual",
"localizations" : {
@@ -12193,25 +12239,25 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "\nHold up! \n\n- Standard render has better performance on most devices now.\n\n- Changing the rendering mode requires restarting the application to ensure a clean start.\n\n- Metal rendering is an experimental feature that may cause performance degradation on some devices.\n\n- Metal render currently is unable to handle HDR content correctly on most devices.\n\n- Metal rendering mode does not support PiP.\n\n- Tapping “Quit Now” means I acknowledge the above information."
"value" : "\nHold up! \n\n- Standard render has better performance on most devices now.\n\n- The change takes effect the next time you start a stream. No app restart is needed.\n\n- Metal rendering is an experimental feature that may cause performance degradation on some devices.\n\n- Metal render currently is unable to handle HDR content correctly on most devices.\n\n- Metal rendering mode does not support PiP.\n\n- Tapping “Apply” means I acknowledge the above information."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n等等!\n\n- 标准渲染模式目前在大多数设备上具有更好的性能。\n\n- 切换渲染模式需要重启应用程序,以确保一次干净的启动。\n\n- Metal 渲染是一项实验性功能,在某些设备上可能会导致性能下降。\n\n- Metal 渲染模式下不支持画中画。\n\n- 目前 Metal 渲染器在大多数设备上无法正确处理 HDR 内容。\n\n点击“立即退出”表示我已知晓并认可以上信息。"
"value" : "\n等等!\n\n- 标准渲染模式目前在大多数设备上具有更好的性能。\n\n- 渲染模式的更改将在下次开始串流时生效,无需重启应用程序。\n\n- Metal 渲染是一项实验性功能,在某些设备上可能会导致性能下降。\n\n- Metal 渲染模式下不支持画中画。\n\n- 目前 Metal 渲染器在大多数设备上无法正确处理 HDR 内容。\n\n点击“应用”表示我已知晓并认可以上信息。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n等等!\n\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式需要重新启动应用程式,以确保干净的启动流程。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。\n\n点选「立即结束」表示我已了解并同意上述说明。"
"value" : "\n等等!\n\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效,无需重新启动应用程式。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。\n\n点选「套用」表示我已了解并同意上述说明。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n等等!\n\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式需要重新启动应用程式,以确保干净的启动流程。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。\n\n点选「立即结束」表示我已了解并同意上述说明。"
"value" : "\n等等!\n\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效,无需重新启动应用程式。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。\n\n点选「套用」表示我已了解并同意上述说明。"
}
}
}
@@ -15619,25 +15665,25 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "- Standard render has better performance on most devices now.\n\n- Changing the rendering mode requires restarting the application to ensure a clean start.\n\n- Metal rendering is an experimental feature that may cause performance degradation on some devices.\n\n- Metal render currently is unable to handle HDR content correctly on most devices.\n\n- Metal rendering mode does not support PiP."
"value" : "- Standard render has better performance on most devices now.\n\n- Changing the rendering mode takes effect the next time you start a stream.\n\n- Metal rendering is an experimental feature that may cause performance degradation on some devices.\n\n- Metal render currently is unable to handle HDR content correctly on most devices.\n\n- Metal rendering mode does not support PiP."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前, 标准渲染模式在大多数设备上具有更好的性能。\n\n- 切换渲染模式需要重启应用程序,以确保一次干净的启动。\n\n- Metal 渲染是一项实验性功能,在某些设备上可能会导致性能下降。\n\n- Metal 渲染模式下不支持画中画。\n\n- 目前 Metal 渲染器在大多数设备上无法正确处理 HDR 内容。\n"
"value" : "- 目前, 标准渲染模式在大多数设备上具有更好的性能。\n\n- 渲染模式的更改将在下次开始串流时生效。\n\n- Metal 渲染是一项实验性功能,在某些设备上可能会导致性能下降。\n\n- Metal 渲染模式下不支持画中画。\n\n- 目前 Metal 渲染器在大多数设备上无法正确处理 HDR 内容。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式需要重新启动应用程式,以确保干净的启动流程。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。"
"value" : "- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式需要重新启动应用程式,以确保干净的启动流程。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。"
"value" : "- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。\n\n- Metal 渲染为实验性功能,可能在某些装置上造成效能下降。\n\n- Metal 渲染模式下不支援画中画。\n\n- Metal 渲染器目前在大多数装置上无法正确处理 HDR 内容。"
}
}
}
@@ -17550,25 +17596,25 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n- Standard render has better performance on most devices now.\n\n- Changing the rendering mode requires restarting the application to ensure a clean start."
"value" : "\n- Standard render has better performance on most devices now.\n\n- The change takes effect the next time you start a stream."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n- 标准渲染模式目前在大多数设备上具有更好的性能。\n\n- 更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?"
"value" : "\n- 目前, 标准渲染模式在大多数设备上具有更好的性能。\n\n- 渲染模式的更改将在下次开始串流时生效。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
"value" : "\n- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?"
"value" : "\n- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。"
}
}
}
@@ -19915,4 +19961,4 @@
}
},
"version" : "1.1"
}
}
+21 -16
View File
@@ -106,7 +106,17 @@ static const NSUInteger MaxFramesInFlight = 3;
// best to tone-map Windows content
static BOOL useEDR = YES;
CFStringRef __currentColorSpace;
// Name of the colorspace most recently applied to the layer, for the stats overlay.
// Class-level because the stats path only has access to the class; guarded by a lock
// since it is written on the render thread and read from other threads, and multiple
// renderer instances can briefly overlap during a session switch.
static NSString *__currentColorSpace;
static void setCurrentColorSpaceName(NSString *name) {
@synchronized ([MetalVideoRenderer class]) {
__currentColorSpace = name;
}
}
@implementation MetalVideoRenderer {
dispatch_queue_t _sq;
@@ -225,11 +235,9 @@ CFStringRef __currentColorSpace;
if (_renderPassDescriptor) {
_renderPassDescriptor = nil;
}
if (__currentColorSpace) {
CFRelease(__currentColorSpace);
__currentColorSpace = NULL;
}
// Do NOT clear __currentColorSpace here: a new renderer instance may already be
// live (session switch) and have set its own value.
}
#if !TARGET_OS_TV
@@ -405,9 +413,11 @@ CFStringRef __currentColorSpace;
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt709Full : k_CscParams_Bt709Lim);
break;
case COLORSPACE_REC_2020: {
// Frames without a format description (e.g. produced for the AVSB
// backend) must not crash us here
CFDictionaryRef ext = [frame getFormatDescExtensions];
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
CFStringRef frame_trc = ext ? CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey) : NULL;
if (frame_trc && CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
isHDR = YES;
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
newPixelFormat = _nonFullHdrPixelFormat;
@@ -478,11 +488,7 @@ CFStringRef __currentColorSpace;
});
}
#endif
if (__currentColorSpace) {
CFRelease(__currentColorSpace);
__currentColorSpace = NULL;
}
__currentColorSpace = CGColorSpaceCopyName(newColorSpace);
setCurrentColorSpaceName(newColorSpace ? CFBridgingRelease(CGColorSpaceCopyName(newColorSpace)) : nil);
CGColorSpaceRelease(newColorSpace);
}
@@ -904,10 +910,9 @@ CFStringRef __currentColorSpace;
}
+ (NSString *)currentColorSpace {
if (__currentColorSpace) {
return (__bridge NSString *)__currentColorSpace;
@synchronized ([MetalVideoRenderer class]) {
return __currentColorSpace;
}
return nil;
}
@end
+3 -1
View File
@@ -24,7 +24,9 @@
@interface MetalView : UIView <CALayerDelegate>
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
// weak: the delegate (MetalViewController) strongly owns this view; a strong
// reference here creates a retain cycle that leaks the whole Metal stack.
@property (nonatomic, weak, nullable) id<MetalViewDelegate> delegate;
@property (nonatomic) float framerate;
- (void)initCommon;
+19 -2
View File
@@ -45,10 +45,20 @@
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
[_renderThread cancel];
Log(LOG_I, @"[MetalView] waiting on renderThread to finish");
while (!_renderThread.isFinished) {
// Bounded wait only: shutdown typically runs on the main thread, and the render
// thread may be blocked in a dispatch_sync onto the main queue (layer colorspace
// changes). Spinning here forever would deadlock; if we time out, the thread
// exits on its own once the main queue is serviced again (it retains the view
// via its block, so this is safe).
CFTimeInterval deadline = CACurrentMediaTime() + 1.0;
while (!_renderThread.isFinished && CACurrentMediaTime() < deadline) {
usleep(100);
}
Log(LOG_I, @"[MetalView] renderThread has finished");
if (_renderThread.isFinished) {
Log(LOG_I, @"[MetalView] renderThread has finished");
} else {
Log(LOG_W, @"[MetalView] renderThread still busy after 1s, letting it exit asynchronously");
}
_renderThread = nil;
}
}
@@ -68,6 +78,13 @@
return;
}
// Never run two render loops at once (the view can move between windows,
// e.g. external display or re-parenting)
if (_renderThread) {
Log(LOG_W, @"[MetalView] movedToWindow with a live renderThread, restarting it");
[self shutdown];
}
// Render on a new thread
_renderThread = [[NSThread alloc] initWithBlock:^{
while (![NSThread currentThread].isCancelled) {
+4
View File
@@ -24,4 +24,8 @@
- (void)pauseRendering;
- (void)resumeRendering;
// Idempotent teardown: stops the render thread, display link and renderer.
// Must be called when the stream session ends; also runs from viewDidDisappear.
- (void)shutdown;
@end
+25 -8
View File
@@ -109,13 +109,18 @@
}
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
// Skip waiting when renderer is paused
if (_renderer.isStopping) {
// Snapshot into a strong local: shutdown (main thread) can nil _renderer while
// the render thread is inside this method.
MetalVideoRenderer *renderer = _renderer;
// Skip waiting when renderer is paused or gone
if (!renderer || renderer.isStopping) {
_frameSlotAcquired = NO;
return;
}
if (@available(iOS 13.0, *)) {
_frameSlotAcquired = [_renderer waitToRenderTo:layer];
_frameSlotAcquired = [renderer waitToRenderTo:layer];
}
if (_frameSlotAcquired) {
@@ -128,22 +133,27 @@
if (!_frameSlotAcquired) {
return;
}
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
// Snapshot for the same reason as in waitToRenderTo
MetalVideoRenderer *renderer = _renderer;
if (!renderer) {
return;
}
CFTimeInterval timeout = (1.0f / _framerate) - renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeoutSync:timeout];
if (!_renderer.isStopping) {
if (!renderer.isStopping) {
// Only render if not paused
if (frame) {
//if (@available(iOS 13.0, *)) {
[_renderer renderFrame:frame toLayer:layer];
[renderer renderFrame:frame toLayer:layer];
//}
} else {
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
dispatch_semaphore_signal([renderer inFlightSemaphore]);
}
} else {
// When paused, we still dequeue frames to prevent accumulation
// but don't render them. Also sleep a bit to reduce CPU usage
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
dispatch_semaphore_signal([renderer inFlightSemaphore]);
usleep(100000);
}
}
@@ -177,11 +187,18 @@
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
[self shutdown];
}
- (void)shutdown {
if (_displayLink) {
[_displayLink invalidate];
_displayLink = nil;
}
// Stop the renderer before the render thread: isStopping makes the thread's
// renderFrame return early, so it can't enter the dispatch_sync-to-main path
// while we wait for it below.
if (_renderer) {
[_renderer shutdown];
_renderer = nil;
+4
View File
@@ -80,6 +80,10 @@ int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void*
void DrCleanup(void)
{
[renderer cleanup];
// Drop the static reference so the old renderer (and its decoder resources)
// doesn't outlive the session; otherwise it stays alive until the next
// Connection init overwrites it, which can interleave with a new session.
renderer = nil;
}
-(BandwidthTracker *) getBwTracker
+31 -5
View File
@@ -68,6 +68,12 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
FramePacingMode _framePacingMode;
bool _enableTimebase;
bool _asyncFrameDequeue;
// Cached UIApplication background state. UIKit's applicationState must only be
// read on the main thread, but the decode path needs it on the VTDecoder queue.
// Updated on the main thread via notifications; a stale read here is harmless
// (it only gates metrics collection).
volatile BOOL _appInBackground;
// CMTime playTime;
// NSTimeInterval previousLinkTime;
@@ -178,7 +184,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
_needRequeuing = _queueSize>0;
_frameQueue = [FrameQueue sharedInstance];
[_frameQueue start];
[_frameQueue startForOwner:self];
[_frameQueue setHighWaterMark:MAX(1, _queueSize)];
[self reinitializeDisplayLayer];
@@ -189,9 +195,29 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
name:@"ScreenChanged"
object:nil];
// Renderer init runs on the main thread, so reading applicationState here is legal.
// The decode queue reads the cached flag instead (UIKit forbids off-main reads).
_appInBackground = ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidEnterBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
return self;
}
- (void)appDidEnterBackground {
_appInBackground = YES;
}
- (void)appWillEnterForeground {
_appInBackground = NO;
}
# pragma mark DisplayLink vsync callback
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate fullRange:(BOOL)fullRange request10BitCodec:(BOOL)request10BitCodec
@@ -476,7 +502,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
// we missed a callback
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
}
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
if (!self->_appInBackground) {
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
}
}
@@ -575,7 +601,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
- (void)cleanup{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_frameQueue stop];
[self->_frameQueue stopForOwner:self];
if (self->_renderingBackend == RENDER_AVSB) {
[self->_displayLink invalidate];
@@ -1195,7 +1221,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
framesDropped += [self->_frameQueue enqueue:outputFrame withSlackSize:3];
}
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
if (!self->_appInBackground) {
static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
@@ -1219,7 +1245,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3];
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
if (!self->_appInBackground) {
static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
+5 -1
View File
@@ -108,7 +108,11 @@ static char FrameInterpolatedKey;
- (CFDictionaryRef)getFormatDescExtensions {
if (!_formatDescExt && _formatDesc) {
_formatDescExt = CFRetain(CMFormatDescriptionGetExtensions(_formatDesc));
// Can legitimately be NULL, and CFRetain(NULL) crashes
CFDictionaryRef ext = CMFormatDescriptionGetExtensions(_formatDesc);
if (ext) {
_formatDescExt = CFRetain(ext);
}
}
return _formatDescExt;
}
+2 -2
View File
@@ -29,8 +29,8 @@ NS_ASSUME_NONNULL_BEGIN
- (CFTimeInterval)estimatedFramerate;
- (int)currentSoftCap;
- (void)waitForEnqueue;
- (void)start;
- (void)stop;
- (void)startForOwner:(id)owner;
- (void)stopForOwner:(id)owner;
@end
+30 -3
View File
@@ -24,6 +24,10 @@
int _currentSoftCap;
dispatch_queue_t _sq;
dispatch_semaphore_t _frameSemaphore;
// The renderer session currently consuming from the queue. A stale session's
// async stop must not pause a newer session's queue.
__weak id _owner;
}
+ (instancetype)sharedInstance {
@@ -301,7 +305,14 @@
- (void)clear {
os_unfair_lock_lock(&_lock);
// Release any frames still held in the ring buffer, otherwise their
// pixel/sample buffers stay retained for the lifetime of the singleton.
for (int i = 0; i < _capacity; i++) {
[_buffer replaceObjectAtIndex:i withObject:[NSNull null]];
}
_head = _tail = _count = 0;
_droppedLast = NO;
_ptsCorrection = CMTimeMake(0, 90000);
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
os_unfair_lock_unlock(&_lock);
}
@@ -336,15 +347,31 @@
return cap;
}
- (void)stop {
- (void)stopForOwner:(id)owner {
@synchronized (self) {
// Ignore stops from a previous session: cleanup is dispatched async and can
// land after a new renderer has already (re)started the queue.
if (_owner != nil && owner != _owner) {
Log(LOG_W, @"FrameQueue stop ignored: %p is not the current owner", owner);
return;
}
_owner = nil;
}
// new frames will no longer be coming in, make sure consumer side is not left waiting
self.paused = YES;
Log(LOG_I, @"FrameQueue stopped");
dispatch_semaphore_signal(_frameSemaphore);
// Don't keep the previous session's frames (and their pixel buffers) alive
[self clear];
}
- (void)start {
// (re)start for a new renderer
- (void)startForOwner:(id)owner {
@synchronized (self) {
_owner = owner;
}
// Drop any frames left over from a previous renderer session; feeding them to a
// renderer with a different backend or video format can crash it.
[self clear];
self.paused = NO;
Log(LOG_I, @"FrameQueue started");
}
@@ -2651,39 +2651,55 @@ BOOL isCustomResolution(int resolutionSelected) {
// Check if the rendering backend has actually changed
if (previousBackend != sender.selectedSegmentIndex) {
// Show alert to prompt user to restart the app
NSString *message = [LocalizationHelper localizedStringForKey: sender.selectedSegmentIndex == 1 ? @"metalRenderTip" : @"standardRenderTip"];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Restart Required"]
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *quitAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Quit Now"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
// The renderer is picked up fresh at every stream start, so the change takes
// effect on the next session — no app restart needed.
void (^applyChange)(void) = ^{
Settings* directSettings = [self->dataMan retrieveSettings];
directSettings.renderingBackend = [NSNumber numberWithInteger:sender.selectedSegmentIndex];
[self->dataMan saveData];
[self saveSettings];
exit(0);
}];
UIAlertAction *laterAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Learn More"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
self.renderingBackendSelector.selectedSegmentIndex = 0;
};
void (^revertChange)(void) = ^{
self.renderingBackendSelector.selectedSegmentIndex = previousBackend;
[self.renderingBackendSelector sendActionsForControlEvents:UIControlEventValueChanged];
NSURL *url = [NSURL URLWithString:[LocalizationHelper localizedStringForKey:@"betterPerformanceLink"]];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}];
[alertController addAction:laterAction];
[alertController addAction:quitAction];
[self presentViewController:alertController animated:YES completion:nil];
};
if (sender.selectedSegmentIndex == RENDER_METAL) {
// Metal is experimental: confirm its caveats before applying
NSString *message = [LocalizationHelper localizedStringForKey:@"metalRenderTip"];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Enable Metal Renderer?"]
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Apply"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
applyChange();
}]];
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Learn More"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
revertChange();
NSURL *url = [NSURL URLWithString:[LocalizationHelper localizedStringForKey:@"betterPerformanceLink"]];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}]];
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
revertChange();
}]];
[self presentViewController:alertController animated:YES completion:nil];
} else {
// Switching back to the standard renderer needs no confirmation
applyChange();
}
}
}
@@ -1244,8 +1244,8 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
- (void)willMoveToParentViewController:(UIViewController *)parent {
// Only cleanup when we're being destroyed
if (parent == nil) {
_streamView = nil;
[_streamView cleanUp];
_streamView = nil;
[_controllerSupport cleanup];
[UIApplication sharedApplication].idleTimerDisabled = NO;
@@ -1255,6 +1255,10 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
_inactivityTimer = nil;
}
if (self.metalViewController) {
// Explicit shutdown: viewDidDisappear is not guaranteed to fire here
// (e.g. teardown while backgrounded), and it's what stops the render
// thread, display link and renderer.
[self.metalViewController shutdown];
[self.metalViewController.view removeFromSuperview];
[self.metalViewController removeFromParentViewController];
self.metalViewController = nil;