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>
222 lines
6.6 KiB
Objective-C
222 lines
6.6 KiB
Objective-C
//
|
|
// MetalViewController.m
|
|
//
|
|
// Created by Andy Grundman.
|
|
// Ported to VoidLink by Acaki.
|
|
// Copyright (c) 2025 Moonlight Stream. All rights reserved.
|
|
//
|
|
|
|
#import "MetalViewController.h"
|
|
#import "FrameQueue.h"
|
|
#import "ImGuiRenderer.h"
|
|
#import "MetalVideoRenderer.h"
|
|
|
|
@implementation MetalViewController {
|
|
FrameQueue *_frameQueue;
|
|
float _framerate;
|
|
TemporarySettings* _currentSettings;
|
|
MetalView *_metalView;
|
|
MetalVideoRenderer *_renderer;
|
|
MetricsHandler _metricsHandler;
|
|
CADisplayLink *_displayLink;
|
|
BOOL _frameSlotAcquired;
|
|
}
|
|
|
|
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate settings:(TemporarySettings* )settings metricsHandler:(MetricsHandler)metricsHandler {
|
|
self = [super init];
|
|
if (self) {
|
|
_bounds = bounds;
|
|
_frameQueue = [FrameQueue sharedInstance];
|
|
_framerate = framerate;
|
|
_currentSettings = settings;
|
|
_metricsHandler = metricsHandler;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)loadView {
|
|
self.view = [[MetalView alloc] initWithFrame:_bounds];
|
|
Log(LOG_I, @"[MetalViewController] created MetalView %@", (MetalView *)self.view);
|
|
}
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
|
|
__block MetalView *view = (MetalView *)self.view;
|
|
if (!view) {
|
|
Log(LOG_E, @"The view attached to MetalViewController isn't a MetalView.");
|
|
return;
|
|
}
|
|
_metalView = view;
|
|
_metalView.delegate = self;
|
|
_metalView.framerate = _framerate;
|
|
|
|
// Select the device to render with.
|
|
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
|
if (!device) {
|
|
Log(LOG_E, @"Metal isn't supported on this device.");
|
|
self.view = [[UIView alloc] initWithFrame:self.view.frame];
|
|
return;
|
|
}
|
|
view.metalLayer.device = device;
|
|
|
|
// Determine supported pixel format before initializing renderer
|
|
// Use TARGET_OS_SIMULATOR to detect simulator environment
|
|
MTLPixelFormat pixelFormat;
|
|
#if TARGET_OS_SIMULATOR
|
|
// iOS Simulator doesn't support BGRA10_XR
|
|
pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
Log(LOG_W, @"Running on iOS Simulator, using BGRA8Unorm pixel format");
|
|
#else
|
|
// On real devices, check if we should enable HDR
|
|
if (_currentSettings.enableHdr) {
|
|
pixelFormat = MTLPixelFormatBGRA10_XR;
|
|
Log(LOG_I, @"HDR enabled, using BGRA10_XR pixel format");
|
|
} else {
|
|
pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
Log(LOG_I, @"HDR disabled, using BGRA8Unorm pixel format");
|
|
}
|
|
#endif
|
|
|
|
// Initialize the renderer.
|
|
MetalVideoRenderer *renderer = [[MetalVideoRenderer alloc] initWithMetalDevice:device
|
|
drawablePixelFormat:pixelFormat
|
|
settings:_currentSettings];
|
|
if (!renderer) {
|
|
Log(LOG_E, @"The renderer couldn't be initialized.");
|
|
return;
|
|
}
|
|
self->_renderer = renderer;
|
|
Log(LOG_I, @"[MetalViewController] viewDidLoad, created renderer: %@", renderer);
|
|
|
|
// Initialize the renderer-dependent view properties.
|
|
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
|
view.metalLayer.maximumDrawableCount = 3;
|
|
|
|
// We need a no-op displaylink timer or iOS can decide to run at 60fps
|
|
// The overhead from this should be minimal.
|
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkHandler:)];
|
|
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
|
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
|
|
} else {
|
|
_displayLink.preferredFramesPerSecond = _framerate;
|
|
}
|
|
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
}
|
|
|
|
- (void)displayLinkHandler:(CADisplayLink *)link {
|
|
// Rendering does not use DisplayLink, this exists to fool iOS into keeping us running at the desired framerate
|
|
}
|
|
|
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
|
// 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];
|
|
}
|
|
|
|
if (_frameSlotAcquired) {
|
|
[_frameQueue waitForEnqueue];
|
|
}
|
|
}
|
|
|
|
/// Draw frame (used by manual loop)
|
|
- (void)renderTo:(nonnull CAMetalLayer *)layer {
|
|
if (!_frameSlotAcquired) {
|
|
return;
|
|
}
|
|
// 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) {
|
|
// Only render if not paused
|
|
if (frame) {
|
|
//if (@available(iOS 13.0, *)) {
|
|
[renderer renderFrame:frame toLayer:layer];
|
|
//}
|
|
} else {
|
|
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]);
|
|
usleep(100000);
|
|
}
|
|
}
|
|
|
|
- (void)drawableResize:(CGSize)size {
|
|
[_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];
|
|
|
|
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;
|
|
}
|
|
|
|
if (_metalView) {
|
|
_metalView.delegate = nil;
|
|
[_metalView shutdown];
|
|
_metalView = nil;
|
|
}
|
|
}
|
|
|
|
#if TARGET_OS_IOS
|
|
// Hides the Home indicator button automatically.
|
|
- (BOOL)prefersHomeIndicatorAutoHidden {
|
|
return YES;
|
|
}
|
|
#endif
|
|
|
|
@end
|