Merge commit '6879bf63791c3422fd88095237bee508524b6959' into frameInterpolation

# Conflicts:
#	VoidLink/Localization/Localizable.xcstrings
#	VoidLink/Stream/VideoDecoderRenderer.m
This commit is contained in:
True Zhuanjia committed 2026-08-09 20:54:04 +08:00
commit b0fd0222df
13 files changed
+340 -76

No files matched your search

+150 -12
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" : { "Arrow direction pad" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "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" : { "enableOswForNativeTouchSwitchTip" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -10923,6 +10969,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" : { "Interval of Multi-Touch Move Events: %d μs" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -12233,25 +12371,25 @@
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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点选「套用」表示我已了解并同意上述说明。"
} }
} }
} }
@@ -15659,25 +15797,25 @@
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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 内容。"
} }
} }
} }
@@ -17613,25 +17751,25 @@
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "\n- 标准渲染模式目前在大多数设备上具有更好的性能。\n\n- 更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?" "value" : "\n- 目前, 标准渲染模式在大多数设备上具有更好的性能。\n\n- 渲染模式的更改将在下次开始串流时生效。"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" "value" : "\n- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。"
} }
}, },
"zh-HK" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "\n- 目前在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" "value" : "\n- 目前,在大多数装置上,標準渲染模式具有较佳的效能。\n\n- 变更渲染模式将在下次开始串流时生效。"
} }
} }
} }
+20 -15
View File
@@ -106,7 +106,17 @@ static const NSUInteger MaxFramesInFlight = 3;
// best to tone-map Windows content // best to tone-map Windows content
static BOOL useEDR = YES; 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 { @implementation MetalVideoRenderer {
dispatch_queue_t _sq; dispatch_queue_t _sq;
@@ -226,10 +236,8 @@ CFStringRef __currentColorSpace;
_renderPassDescriptor = nil; _renderPassDescriptor = nil;
} }
if (__currentColorSpace) { // Do NOT clear __currentColorSpace here: a new renderer instance may already be
CFRelease(__currentColorSpace); // live (session switch) and have set its own value.
__currentColorSpace = NULL;
}
} }
#if !TARGET_OS_TV #if !TARGET_OS_TV
@@ -405,9 +413,11 @@ CFStringRef __currentColorSpace;
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt709Full : k_CscParams_Bt709Lim); paramBuffer.cscParams = (fullRange ? k_CscParams_Bt709Full : k_CscParams_Bt709Lim);
break; break;
case COLORSPACE_REC_2020: { 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]; CFDictionaryRef ext = [frame getFormatDescExtensions];
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey); CFStringRef frame_trc = ext ? CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey) : NULL;
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) { if (frame_trc && CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
isHDR = YES; isHDR = YES;
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace); newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
newPixelFormat = _nonFullHdrPixelFormat; newPixelFormat = _nonFullHdrPixelFormat;
@@ -478,11 +488,7 @@ CFStringRef __currentColorSpace;
}); });
} }
#endif #endif
if (__currentColorSpace) { setCurrentColorSpaceName(newColorSpace ? CFBridgingRelease(CGColorSpaceCopyName(newColorSpace)) : nil);
CFRelease(__currentColorSpace);
__currentColorSpace = NULL;
}
__currentColorSpace = CGColorSpaceCopyName(newColorSpace);
CGColorSpaceRelease(newColorSpace); CGColorSpaceRelease(newColorSpace);
} }
@@ -904,10 +910,9 @@ CFStringRef __currentColorSpace;
} }
+ (NSString *)currentColorSpace { + (NSString *)currentColorSpace {
if (__currentColorSpace) { @synchronized ([MetalVideoRenderer class]) {
return (__bridge NSString *)__currentColorSpace; return __currentColorSpace;
} }
return nil;
} }
@end @end
+3 -1
View File
@@ -24,7 +24,9 @@
@interface MetalView : UIView <CALayerDelegate> @interface MetalView : UIView <CALayerDelegate>
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer; @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; @property (nonatomic) float framerate;
- (void)initCommon; - (void)initCommon;
+19 -2
View File
@@ -45,10 +45,20 @@
Log(LOG_I, @"[MetalView] sending renderThread a cancel message"); Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
[_renderThread cancel]; [_renderThread cancel];
Log(LOG_I, @"[MetalView] waiting on renderThread to finish"); 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); 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; _renderThread = nil;
} }
} }
@@ -68,6 +78,13 @@
return; 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 // Render on a new thread
_renderThread = [[NSThread alloc] initWithBlock:^{ _renderThread = [[NSThread alloc] initWithBlock:^{
while (![NSThread currentThread].isCancelled) { while (![NSThread currentThread].isCancelled) {
+4
View File
@@ -24,4 +24,8 @@
- (void)pauseRendering; - (void)pauseRendering;
- (void)resumeRendering; - (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 @end
+25 -8
View File
@@ -109,13 +109,18 @@
} }
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer { - (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
// Skip waiting when renderer is paused // Snapshot into a strong local: shutdown (main thread) can nil _renderer while
if (_renderer.isStopping) { // 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; return;
} }
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
_frameSlotAcquired = [_renderer waitToRenderTo:layer]; _frameSlotAcquired = [renderer waitToRenderTo:layer];
} }
if (_frameSlotAcquired) { if (_frameSlotAcquired) {
@@ -128,22 +133,27 @@
if (!_frameSlotAcquired) { if (!_frameSlotAcquired) {
return; 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]; Frame *frame = [_frameQueue dequeueWithTimeoutSync:timeout];
if (!_renderer.isStopping) { if (!renderer.isStopping) {
// Only render if not paused // Only render if not paused
if (frame) { if (frame) {
//if (@available(iOS 13.0, *)) { //if (@available(iOS 13.0, *)) {
[_renderer renderFrame:frame toLayer:layer]; [renderer renderFrame:frame toLayer:layer];
//} //}
} else { } else {
dispatch_semaphore_signal([_renderer inFlightSemaphore]); dispatch_semaphore_signal([renderer inFlightSemaphore]);
} }
} else { } else {
// When paused, we still dequeue frames to prevent accumulation // When paused, we still dequeue frames to prevent accumulation
// but don't render them. Also sleep a bit to reduce CPU usage // 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); usleep(100000);
} }
} }
@@ -177,11 +187,18 @@
Log(LOG_I, @"[MetalViewController] viewDidDisappear"); Log(LOG_I, @"[MetalViewController] viewDidDisappear");
[self shutdown];
}
- (void)shutdown {
if (_displayLink) { if (_displayLink) {
[_displayLink invalidate]; [_displayLink invalidate];
_displayLink = nil; _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) { if (_renderer) {
[_renderer shutdown]; [_renderer shutdown];
_renderer = nil; _renderer = nil;
+4
View File
@@ -80,6 +80,10 @@ int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void*
void DrCleanup(void) void DrCleanup(void)
{ {
[renderer cleanup]; [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 -(BandwidthTracker *) getBwTracker
+31 -5
View File
@@ -78,6 +78,12 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
bool _enableTimebase; bool _enableTimebase;
bool _asyncFrameDequeue; 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; // CMTime playTime;
// NSTimeInterval previousLinkTime; // NSTimeInterval previousLinkTime;
} }
@@ -207,7 +213,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
_needRequeuing = _queueSize>0; _needRequeuing = _queueSize>0;
_frameQueue = [FrameQueue sharedInstance]; _frameQueue = [FrameQueue sharedInstance];
[_frameQueue start]; [_frameQueue startForOwner:self];
[_frameQueue setHighWaterMark:MAX(1, _queueSize)]; [_frameQueue setHighWaterMark:MAX(1, _queueSize)];
[self reinitializeDisplayLayer]; [self reinitializeDisplayLayer];
@@ -222,6 +228,18 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
sActiveRenderer = self; sActiveRenderer = self;
} }
// 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; return self;
} }
@@ -327,6 +345,14 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
}); });
} }
- (void)appDidEnterBackground {
_appInBackground = YES;
}
- (void)appWillEnterForeground {
_appInBackground = NO;
}
# pragma mark DisplayLink vsync callback # pragma mark DisplayLink vsync callback
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate fullRange:(BOOL)fullRange request10BitCodec:(BOOL)request10BitCodec - (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate fullRange:(BOOL)fullRange request10BitCodec:(BOOL)request10BitCodec
@@ -570,7 +596,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
// we missed a callback // we missed a callback
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0); // 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]; [[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
} }
} }
@@ -675,7 +701,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self->_frameQueue stop]; [self->_frameQueue stopForOwner:self];
if (self->_renderingBackend == RENDER_AVSB) { if (self->_renderingBackend == RENDER_AVSB) {
[self->_displayLink invalidate]; [self->_displayLink invalidate];
@@ -1310,7 +1336,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
framesDropped += [self->_frameQueue enqueue:outputFrame withSlackSize:3]; framesDropped += [self->_frameQueue enqueue:outputFrame withSlackSize:3];
} }
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { if (!self->_appInBackground) {
static PlotMetrics frameQueueMetrics = {}; static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics]; [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics]; [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
@@ -1334,7 +1360,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3]; int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3];
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { if (!self->_appInBackground) {
static PlotMetrics frameQueueMetrics = {}; static PlotMetrics frameQueueMetrics = {};
[[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics]; [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics];
[self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics]; [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics];
+5 -1
View File
@@ -108,7 +108,11 @@ static char FrameInterpolatedKey;
- (CFDictionaryRef)getFormatDescExtensions { - (CFDictionaryRef)getFormatDescExtensions {
if (!_formatDescExt && _formatDesc) { 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; return _formatDescExt;
} }
+2 -2
View File
@@ -29,8 +29,8 @@ NS_ASSUME_NONNULL_BEGIN
- (CFTimeInterval)estimatedFramerate; - (CFTimeInterval)estimatedFramerate;
- (int)currentSoftCap; - (int)currentSoftCap;
- (void)waitForEnqueue; - (void)waitForEnqueue;
- (void)start; - (void)startForOwner:(id)owner;
- (void)stop; - (void)stopForOwner:(id)owner;
@end @end
+30 -3
View File
@@ -24,6 +24,10 @@
int _currentSoftCap; int _currentSoftCap;
dispatch_queue_t _sq; dispatch_queue_t _sq;
dispatch_semaphore_t _frameSemaphore; 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 { + (instancetype)sharedInstance {
@@ -301,7 +305,14 @@
- (void)clear { - (void)clear {
os_unfair_lock_lock(&_lock); 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; _head = _tail = _count = 0;
_droppedLast = NO;
_ptsCorrection = CMTimeMake(0, 90000);
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512]; _frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
os_unfair_lock_unlock(&_lock); os_unfair_lock_unlock(&_lock);
} }
@@ -336,15 +347,31 @@
return cap; 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 // new frames will no longer be coming in, make sure consumer side is not left waiting
self.paused = YES; self.paused = YES;
Log(LOG_I, @"FrameQueue stopped"); Log(LOG_I, @"FrameQueue stopped");
dispatch_semaphore_signal(_frameSemaphore); dispatch_semaphore_signal(_frameSemaphore);
// Don't keep the previous session's frames (and their pixel buffers) alive
[self clear];
} }
- (void)start { - (void)startForOwner:(id)owner {
// (re)start for a new renderer @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; self.paused = NO;
Log(LOG_I, @"FrameQueue started"); Log(LOG_I, @"FrameQueue started");
} }
@@ -2681,39 +2681,55 @@ BOOL isCustomResolution(int resolutionSelected) {
// Check if the rendering backend has actually changed // Check if the rendering backend has actually changed
if (previousBackend != sender.selectedSegmentIndex) { if (previousBackend != sender.selectedSegmentIndex) {
// Show alert to prompt user to restart the app // The renderer is picked up fresh at every stream start, so the change takes
NSString *message = [LocalizationHelper localizedStringForKey: sender.selectedSegmentIndex == 1 ? @"metalRenderTip" : @"standardRenderTip"]; // effect on the next session — no app restart needed.
void (^applyChange)(void) = ^{
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) {
Settings* directSettings = [self->dataMan retrieveSettings]; Settings* directSettings = [self->dataMan retrieveSettings];
directSettings.renderingBackend = [NSNumber numberWithInteger:sender.selectedSegmentIndex]; directSettings.renderingBackend = [NSNumber numberWithInteger:sender.selectedSegmentIndex];
[self->dataMan saveData]; [self->dataMan saveData];
[self saveSettings]; [self saveSettings];
};
exit(0); void (^revertChange)(void) = ^{
}]; self.renderingBackendSelector.selectedSegmentIndex = previousBackend;
UIAlertAction *laterAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Learn More"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
self.renderingBackendSelector.selectedSegmentIndex = 0;
[self.renderingBackendSelector sendActionsForControlEvents:UIControlEventValueChanged]; [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]; if (sender.selectedSegmentIndex == RENDER_METAL) {
[alertController addAction:quitAction]; // Metal is experimental: confirm its caveats before applying
[self presentViewController:alertController animated:YES completion:nil]; 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 { - (void)willMoveToParentViewController:(UIViewController *)parent {
// Only cleanup when we're being destroyed // Only cleanup when we're being destroyed
if (parent == nil) { if (parent == nil) {
_streamView = nil;
[_streamView cleanUp]; [_streamView cleanUp];
_streamView = nil;
[_controllerSupport cleanup]; [_controllerSupport cleanup];
[UIApplication sharedApplication].idleTimerDisabled = NO; [UIApplication sharedApplication].idleTimerDisabled = NO;
@@ -1255,6 +1255,10 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil
_inactivityTimer = nil; _inactivityTimer = nil;
} }
if (self.metalViewController) { 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.view removeFromSuperview];
[self.metalViewController removeFromParentViewController]; [self.metalViewController removeFromParentViewController];
self.metalViewController = nil; self.metalViewController = nil;