Merge commit '6879bf63791c3422fd88095237bee508524b6959' into frameInterpolation

# Conflicts:
#	VoidLink/Localization/Localizable.xcstrings
#	VoidLink/Stream/VideoDecoderRenderer.m
This commit is contained in:
True Zhuanjia
2026-08-09 20:54:04 +08:00
13 changed files with 343 additions and 79 deletions
+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" : {
"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" : {
@@ -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" : {
"extractionState" : "manual",
"localizations" : {
@@ -12233,25 +12371,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点选「套用」表示我已了解并同意上述说明。"
}
}
}
@@ -15659,25 +15797,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 内容。"
}
}
}
@@ -17613,25 +17751,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- 变更渲染模式将在下次开始串流时生效。"
}
}
}
+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
@@ -77,6 +77,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;
@@ -207,7 +213,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];
@@ -222,6 +228,18 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
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;
}
@@ -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
- (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
// 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];
}
}
@@ -675,7 +701,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
}
dispatch_async(dispatch_get_main_queue(), ^{
[self->_frameQueue stop];
[self->_frameQueue stopForOwner:self];
if (self->_renderingBackend == RENDER_AVSB) {
[self->_displayLink invalidate];
@@ -1310,7 +1336,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];
@@ -1334,7 +1360,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");
}
@@ -2681,39 +2681,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;