MetalVideoRenderer: EDR support, enabling better HDR output and tone-mapping regardless of screen brightness. This commit also includes some fixes around stream shutdown, but restarting the app is still needed if the stream stops.

This commit is contained in:
Andy Grundman
2025-07-20 19:06:21 +08:00
committed by Acaki
parent 034ee52eec
commit 1e24731b60
17 changed files with 426 additions and 366 deletions
+4 -1
View File
@@ -40,7 +40,7 @@
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; };
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */; };
97E20DBF2E21637B000D5DCA /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DBE2E21637B000D5DCA /* SceneDelegate.m */; };
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */ = {isa = PBXBuildFile; fileRef = 9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */; };
97E20DCF2E21647A000D5DCA /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCD2E21647A000D5DCA /* Shaders.metal */; };
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCC2E21647A000D5DCA /* MetalViewController.m */; };
97E20DD12E21647A000D5DCA /* MetalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCA2E21647A000D5DCA /* MetalView.m */; };
@@ -211,6 +211,7 @@
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalizationHelper.h; sourceTree = "<group>"; };
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalizationHelper.m; sourceTree = "<group>"; };
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "Shaders-Linear.metal"; sourceTree = "<group>"; };
97E20DBD2E21637B000D5DCA /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
97E20DBE2E21637B000D5DCA /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
97E20DC62E21647A000D5DCA /* MetalConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MetalConfig.h; sourceTree = "<group>"; };
@@ -469,6 +470,7 @@
97E20DCE2E21647A000D5DCA /* Metal */ = {
isa = PBXGroup;
children = (
9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */,
97E20DC62E21647A000D5DCA /* MetalConfig.h */,
97E20DC72E21647A000D5DCA /* MetalVideoRenderer.h */,
97E20DC82E21647A000D5DCA /* MetalVideoRenderer.m */,
@@ -1022,6 +1024,7 @@
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
FB1D59971BBCCB6400F482CA /* ComputerScrollView.m in Sources */,
+5 -7
View File
@@ -1,5 +1,6 @@
#import <QuartzCore/CAMetalLayer.h>
#import <QuartzCore/QuartzCore.h>
#import <simd/simd.h>
#import "ConnectionCallbacks.h"
#import "Frame.h"
#import "Plot.h"
@@ -9,17 +10,14 @@
@property (atomic) CFTimeInterval averageGPUTime;
@property (nonatomic) NSUInteger sampleCount;
@property (nonatomic) MTLPixelFormat colorPixelFormat;
@property (nonatomic, nonnull) CGColorSpaceRef colorspace;
@property (nonatomic) CFTimeInterval lastPresented;
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
@property (atomic) BOOL isStopping;
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device
drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat
framerate:(float)framerate;
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate;
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
- (void)drawableResize:(CGSize)drawableSize;
- (void)plotFrametime:(CFTimeInterval)presentedTime;
- (void)stop;
- (void)discardNextDrawable;
- (void)shutdown;
@end
+185 -118
View File
@@ -1,7 +1,10 @@
#import "MetalVideoRenderer.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreVideo/CoreVideo.h>
#import <Foundation/Foundation.h>
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <simd/simd.h>
#import "ImGuiPlots.h"
#include <Limelight.h>
@@ -81,7 +84,6 @@ static const struct CscParams k_CscParams_Bt2020Full_10bit = {
{1.0f, -0.1646f, -0.5714f},
{1.0f, 1.8814f, 0.0f},
},
{0.0f, 512.0f / 1023.0f, 512.0f / 1023.0f},
};
@@ -101,10 +103,10 @@ static const NSUInteger MaxFramesInFlight = 3;
id<MTLLibrary> _shaderLibrary;
id<MTLRenderPipelineState> _videoPipelineState[MAX_VIDEO_PLANES];
MTLRenderPassDescriptor *_renderPassDescriptor;
id<MTLTexture> _videoTexture;
CVMetalTextureCacheRef _textureCache;
CVMetalTextureRef _cvMetalTextures[MAX_VIDEO_PLANES];
CGFloat _currentEDRHeadroom;
int _lastColorSpace;
BOOL _lastFullRange;
size_t _lastFrameWidth;
@@ -113,8 +115,9 @@ static const NSUInteger MaxFramesInFlight = 3;
size_t _lastDrawableHeight;
id<MTLBuffer> _CscParamsBuffer;
id<MTLBuffer> _VideoVertexBuffer;
CFTimeInterval _lastPresented;
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
dispatch_semaphore_t _inFlightSemaphore;
}
- (instancetype)initWithMetalDevice:(id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate {
@@ -124,14 +127,15 @@ static const NSUInteger MaxFramesInFlight = 3;
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
_averageGPUTime = (1.0f / framerate) / 2;
_device = device;
_nextDrawable = nil;
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
_colorspace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
_framerate = framerate;
_commandQueue = [_device newCommandQueue];
_currentEDRHeadroom = 1.0f;
_lastColorSpace = -1;
_lastFullRange = NO;
_lastPresented = 0;
_lastPresented = 0.0f;
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
_isStopping = NO;
CFStringRef keys[1] = {kCVMetalTextureUsage};
NSUInteger values[1] = {MTLTextureUsageShaderRead};
@@ -148,63 +152,94 @@ static const NSUInteger MaxFramesInFlight = 3;
}
- (void)dealloc {
// Release the Core Foundation texture cache, which is not managed by ARC.
if (_textureCache) {
CFRelease(_textureCache);
_textureCache = NULL;
if (_CscParamsBuffer) {
_CscParamsBuffer = nil;
}
if (_VideoVertexBuffer) {
_VideoVertexBuffer = nil;
}
// Safely release any textures that might still be referenced.
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
if (_cvMetalTextures[i]) {
CFRelease(_cvMetalTextures[i]);
_cvMetalTextures[i] = NULL;
if (_videoPipelineState[i]) {
_videoPipelineState[i] = nil;
}
}
// ARC will handle the rest of the Objective-C objects like _CscParamsBuffer.
if (_renderPassDescriptor) {
_renderPassDescriptor = nil;
}
}
#if !TARGET_OS_TV
- (void)applyEDRFromFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
CFDictionaryRef ext = [frame getFormatDescExtensions];
Log(LOG_I, @"ext: %@", ext);
CFDataRef masteringData = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_MasteringDisplayColorVolume);
CFDataRef contentDataRef = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_ContentLightLevelInfo);
if (masteringData) {
Log(LOG_I, @"ext MDCV %@", masteringData);
}
if (contentDataRef) {
Log(LOG_I, @"ext CLLI %@", contentDataRef);
}
if (masteringData && CFDataGetLength(masteringData) == 24 && contentDataRef && CFDataGetLength(contentDataRef) == 4) {
NSData *displayData = (__bridge NSData *)masteringData;
NSData *contentData = (__bridge NSData *)contentDataRef;
layer.wantsExtendedDynamicRangeContent = YES;
layer.pixelFormat = MTLPixelFormatRGBA16Float;
CFStringRef name = kCGColorSpaceExtendedLinearITUR_2020;
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
layer.colorspace = colorspace;
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithDisplayInfo:displayData contentInfo:contentData opticalOutputScale:100.0f];
Log(LOG_I, @"EDRMetadata set from MDCV %@ and CLLI %@", displayData, contentData);
- (void)reportMaxEDRHeadroom {
CGFloat maxHeadroom = 1.0f;
#if TARGET_OS_OSX
maxHeadroom = [[NSScreen mainScreen] maximumPotentialExtendedDynamicRangeColorComponentValue];
#else
maxHeadroom = [[UIScreen mainScreen] potentialEDRHeadroom];
#endif
if (maxHeadroom > 1.0) {
LogOnce(LOG_I, @"Display supports EDR with a max headroom of %.1f", maxHeadroom);
} else {
LogOnce(LOG_I, @"Display does not support EDR");
}
}
- (void)pollCurrentEDRHeadroom {
CGFloat headroom = 1.0f;
#if TARGET_OS_OSX
headroom = [[NSScreen mainScreen] maximumExtendedDynamicRangeColorComponentValue];
#else
headroom = [[UIScreen mainScreen] currentEDRHeadroom];
#endif
if (headroom != _currentEDRHeadroom) {
Log(LOG_I, @"EDR headroom changed to %.1f", headroom);
_currentEDRHeadroom = headroom;
}
}
- (void)setInitialEDRMetadata {
}
- (void)applyEDRFromFrame:(Frame *)frame withColorspace:(int)colorspace toLayer:(CAMetalLayer *)layer {
[self reportMaxEDRHeadroom];
[self setInitialEDRMetadata];
CFDictionaryRef ext = [frame getFormatDescExtensions];
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
// These can only be changed on the main thread
dispatch_sync(dispatch_get_main_queue(), ^{
layer.wantsExtendedDynamicRangeContent = YES;
layer.pixelFormat = MTLPixelFormatRGBA16Float;
CFStringRef name = kCGColorSpaceExtendedLinearITUR_2020;
CFStringRef name;
switch (colorspace) {
case COLORSPACE_REC_2020:
name = kCGColorSpaceExtendedLinearITUR_2020;
break;
case COLORSPACE_REC_601:
name = kCGColorSpaceExtendedLinearSRGB;
break;
case COLORSPACE_REC_709:
default:
name = kCGColorSpaceExtendedLinearSRGB;
break;
}
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
layer.colorspace = colorspace;
CGColorSpaceRelease(colorspace);
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0.0005f maxLuminance:1000.0f opticalOutputScale:100.0f];
Log(LOG_I, @"EDRMetadata set for 1000 nits");
}
CFDataRef masteringDisplayColorVolume = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferMasteringDisplayColorVolumeKey, nil);
CFDataRef contentLightLevel = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferContentLightLevelInfoKey, nil);
if (masteringDisplayColorVolume) {
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithDisplayInfo:(__bridge NSData *)masteringDisplayColorVolume
contentInfo:contentLightLevel ? (__bridge NSData *)contentLightLevel : nil
opticalOutputScale:100.0f];
} else {
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0.005f maxLuminance:1000.0f opticalOutputScale:100.0f];
}
LogOnce(LOG_I, @"EDRMetadata set to colorspace %@, transfer function %@, %@", layer.colorspace, frame_trc, layer.EDRMetadata);
});
}
#endif
@@ -293,17 +328,21 @@ static const NSUInteger MaxFramesInFlight = 3;
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
}
// These can only be changed on the main thread
dispatch_sync(dispatch_get_main_queue(), ^{
if ([CAEDRMetadata isAvailable]) {
[self applyEDRFromFrame:frame withColorspace:colorspace toLayer:layer];
} else {
// These can only be changed on the main thread
dispatch_sync(dispatch_get_main_queue(), ^{
#if !TARGET_OS_TV
if (isHDR) {
layer.wantsExtendedDynamicRangeContent = YES;
}
if (isHDR) {
layer.wantsExtendedDynamicRangeContent = YES;
}
#endif
layer.colorspace = newColorSpace;
layer.pixelFormat = newPixelFormat;
});
CGColorSpaceRelease(newColorSpace);
layer.colorspace = newColorSpace;
layer.pixelFormat = newPixelFormat;
});
CGColorSpaceRelease(newColorSpace);
}
}
// Create the new colorspace parameter buffer for our fragment shader
@@ -383,12 +422,13 @@ static const NSUInteger MaxFramesInFlight = 3;
return YES;
}
- (void)discardNextDrawable {
_nextDrawable = nil;
}
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
@autoreleasepool {
if (self.isStopping) {
Log(LOG_I, @"XXX Metal renderThread is stopping. returning from renderFrame");
return;
}
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer
{ @autoreleasepool {
// Handle changes to the frame's colorspace from last time we rendered
BOOL layerDidChange = NO;
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
@@ -402,11 +442,6 @@ static const NSUInteger MaxFramesInFlight = 3;
FQLog(LOG_I, @"[%d / %.3f ms] Metal frame rendering", frame.frameNumber, frame.pts);
#if !TARGET_OS_TV
// Experimental EDR handling based on frame metadata
//[self applyEDRFromFrame:frame toLayer:layer];
#endif
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
assert(planes <= MAX_VIDEO_PLANES);
@@ -419,23 +454,38 @@ static const NSUInteger MaxFramesInFlight = 3;
if (!_videoPipelineState[planes]) {
MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
pipelineDesc.vertexFunction = [defaultLibrary newFunctionWithName:@"vs_draw"];
// RGB shaders
id<MTLFunction> vertexVsDraw = [defaultLibrary newFunctionWithName:@"vs_draw"];
// linear shaders
id<MTLFunction> yuvToLinear = [defaultLibrary newFunctionWithName:@"yuvToLinear"];
// Determine if this is 10-bit based on the layer's pixel format (after colorspace update)
BOOL is10Bit = (layer.pixelFormat == MTLPixelFormatBGR10A2Unorm);
Log(LOG_I, @"Layer pixel format: %lu, is10Bit: %@", (unsigned long)layer.pixelFormat, is10Bit ? @"YES" : @"NO");
NSString *fragmentShaderName;
if (planes == 2) {
fragmentShaderName = is10Bit ? @"ps_draw_biplanar_10bit" : @"ps_draw_biplanar_8bit";
} else {
fragmentShaderName = is10Bit ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
}
Log(LOG_I, @"Rendering frame with %zu planes, using shader: %@", planes, fragmentShaderName);
pipelineDesc.fragmentFunction = [defaultLibrary newFunctionWithName:fragmentShaderName];
pipelineDesc.colorAttachments[0].pixelFormat = layer.pixelFormat;
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
Log(LOG_I, @"Metal pipeline state for %zu planes with pixel format %@",
planes, layer.pixelFormat == MTLPixelFormatRGBA16Float ? @"MTLPixelFormatRGBA16Float" : @"MTLPixelFormatBGR10A2Unorm");
if (layer.pixelFormat == MTLPixelFormatRGBA16Float) {
// 4:2:0 or 4:4:4 YUV -> BT.2020 RGB -> linear float
pipelineDesc.vertexFunction = vertexVsDraw;
pipelineDesc.fragmentFunction = yuvToLinear;
} else {
// 4:2:0 or 4:4:4 YUV -> BT.2020 RGB
pipelineDesc.vertexFunction = vertexVsDraw;
pipelineDesc.fragmentFunction = [defaultLibrary newFunctionWithName:fragmentShaderName];
}
NSError *error = nil;
_videoPipelineState[planes] = [_device newRenderPipelineStateWithDescriptor:pipelineDesc error:&error];
@@ -468,18 +518,15 @@ static const NSUInteger MaxFramesInFlight = 3;
return;
}
if (_cvMetalTextures[i]) {
CVBufferRelease(_cvMetalTextures[i]);
}
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_textureCache,
frame.pixelBuffer,
NULL,
fmt,
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
i,
&_cvMetalTextures[i]);
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_textureCache,
frame.pixelBuffer,
NULL,
fmt,
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
i,
&_cvMetalTextures[i]);
if (err != kCVReturnSuccess) {
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed: %d", err);
return;
@@ -501,11 +548,45 @@ static const NSUInteger MaxFramesInFlight = 3;
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
}
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
// if (layer.pixelFormat == MTLPixelFormatRGBA16Float) {
// [self pollCurrentEDRHeadroom];
// [renderEncoder setFragmentBytes:&_currentEDRHeadroom length:sizeof(CGFloat) atIndex:0];
// }
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
[renderEncoder endEncoding];
__block MetalVideoRenderer *strongSelf = self;
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
if (strongSelf.lastPresented > 0.0f) {
CFTimeInterval frametime = d.presentedTime - strongSelf.lastPresented;
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
}
strongSelf.lastPresented = d.presentedTime;
}];
// signal semaphore, compute GPU time average, and clear textures
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
dispatch_semaphore_signal(block_semaphore);
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
const double alpha = 0.25f;
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
// Free textures after completion of rendering
for (size_t i = 0; i < planes; i++) {
if (self->_cvMetalTextures[i]) {
CFRelease(self->_cvMetalTextures[i]);
self->_cvMetalTextures[i] = nil;
}
}
CVMetalTextureCacheFlush(self->_textureCache, 0);
}];
#if TARGET_OS_SIMULATOR
[commandBuffer presentDrawable:drawable];
#else
@@ -515,39 +596,28 @@ static const NSUInteger MaxFramesInFlight = 3;
[commandBuffer commit];
__weak typeof(self) self_ = self;
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
if (self_) {
[self_ plotFrametime:d.presentedTime];
}
}];
const CFTimeInterval GPUTime = commandBuffer.GPUEndTime - commandBuffer.GPUStartTime;
const double alpha = 0.25f;
_averageGPUTime = (GPUTime * alpha) + (_averageGPUTime * (1.0 - alpha));
// Free textures after completion of rendering
for (size_t i = 0; i < planes; i++) {
if (_cvMetalTextures[i]) {
CVBufferRelease(_cvMetalTextures[i]);
_cvMetalTextures[i] = nil;
}
}
CVMetalTextureCacheFlush(_textureCache, 0);
// Wait for the command buffer to complete and free our CVMetalTextureCache references
[commandBuffer waitUntilCompleted];
}
}
- (void)plotFrametime:(CFTimeInterval)presentedTime {
if (_lastPresented > 0) {
CFTimeInterval frametime = presentedTime - _lastPresented;
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
if (!self.isStopping) {
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
dispatch_semaphore_wait(_inFlightSemaphore, timeout);
}
_lastPresented = presentedTime;
}
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
- (void)shutdown {
Log(LOG_I, @"XXX MetalVideoRenderer shutodwn");
self.isStopping = YES;
// Ensure no rendering is in flight
for (NSUInteger i = 0; i < MaxFramesInFlight; i++) {
dispatch_semaphore_signal(_inFlightSemaphore);
}
}
/// Responds to the drawable's size or orientation changes.
@@ -558,7 +628,4 @@ static const NSUInteger MaxFramesInFlight = 3;
- (void)resize:(CGSize)size {
}
- (void)stop {
}
@end
+2 -4
View File
@@ -17,6 +17,7 @@
- (void)drawableResize:(CGSize)size;
- (void)renderTo:(nonnull CAMetalLayer *)layer;
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
- (void)shutdown;
@end
@@ -29,12 +30,9 @@
@property (nonatomic) float framerate;
- (void)initCommon;
- (void)shutdown;
#if AUTOMATICALLY_RESIZE
- (void)resizeDrawable:(CGFloat)scaleFactor;
#endif
- (void)stop;
- (void)pause;
- (void)resume;
@end
+52 -86
View File
@@ -8,10 +8,6 @@
@implementation MetalView {
// The secondary thread containing the render loop.
NSThread *_renderThread;
// The flag to indicate that rendering needs to cease on the main thread.
BOOL _continueRunLoop;
dispatch_semaphore_t _renderThreadSemaphore;
}
#pragma mark - Initialization and Setup.
@@ -42,7 +38,17 @@
_metalLayer = (CAMetalLayer *)self.layer;
self.layer.delegate = self;
_renderThreadSemaphore = dispatch_semaphore_create(0);
}
- (void)shutdown {
if (_renderThread) {
[_renderThread cancel];
// wait for thread to exist
while (!_renderThread.isFinished) {
Log(LOG_I, @"XXX MetalView waiting on renderThread to finish");
usleep(100);
}
}
}
#if TARGET_OS_IOS || TARGET_OS_TV
@@ -63,71 +69,52 @@
}
#endif // END TARGET_OS_IOS || TARGET_OS_TV
- (void)startRenderThread {
@synchronized(self) {
// Don't start a new thread if one is already running
if (_renderThread) {
return;
}
_continueRunLoop = YES;
_renderThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_renderThread start];
}
}
- (void)movedToWindow {
if (self.window) {
// The view was added to a window, so start rendering.
[self resume];
// Notify the delegate of the drawable's size.
CGSize defaultDrawableSize = self.bounds.size;
defaultDrawableSize.width *= self.layer.contentsScale;
defaultDrawableSize.height *= self.layer.contentsScale;
[self.delegate drawableResize:defaultDrawableSize];
} else {
// The view was removed from a window, so stop rendering.
[self pause];
}
}
if (!self.window) {
return;
- (void)pause {
// Pausing is implemented by simply stopping the render thread.
[self stop];
}
- (void)resume {
// Resuming is implemented by starting a new render thread.
[self startRenderThread];
}
- (void)runThread {
// The system sets the '_continueRunLoop' ivar outside this thread, so it needs to synchronize. Create a
// 'continueRunLoop' local var that the system can set from the _continueRunLoop ivar in a @synchronized block.
BOOL continueRunLoop = YES;
// Begin the run loop.
while (continueRunLoop) {
@autoreleasepool {
[_delegate waitToRenderTo:_metalLayer];
@synchronized(self) {
continueRunLoop = _continueRunLoop;
}
if (!continueRunLoop) {
break;
}
[_delegate renderTo:_metalLayer];
@synchronized(self) {
continueRunLoop = _continueRunLoop;
// We have been removed
if (_renderThread) {
[_renderThread cancel];
// wait for thread to exist
while (!_renderThread.isFinished) {
Log(LOG_I, @"XXX MetalView waiting on renderThread to finish");
usleep(100);
}
}
return;
}
dispatch_semaphore_signal(self->_renderThreadSemaphore);
// Render on a new thread
_renderThread = [[NSThread alloc] initWithBlock:^{
while (![NSThread currentThread].isCancelled) {
@autoreleasepool {
[self.delegate waitToRenderTo:self.metalLayer];
[self.delegate renderTo:self.metalLayer];
}
}
Log(LOG_I, @"XXX Metal renderThread shutting down");
}];
_renderThread.name = @"MetalVideoRenderer";
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_renderThread start];
// Perform any actions that need to know the size and scale of the drawable. When UIKit calls
// didMoveToWindow after the view initialization, this is the first opportunity to notify
// components of the drawable's size.
#if AUTOMATICALLY_RESIZE
#if TARGET_OS_IOS || TARGET_OS_TV
[self resizeDrawable:self.window.screen.nativeScale];
#else
[self resizeDrawable:self.window.screen.backingScaleFactor];
#endif
#else
// Notify the delegate of the default drawable size when the system can calculate it.
CGSize defaultDrawableSize = self.bounds.size;
defaultDrawableSize.width *= self.layer.contentsScale;
defaultDrawableSize.height *= self.layer.contentsScale;
[self.delegate drawableResize:defaultDrawableSize];
#endif
}
#pragma mark - Resizing
@@ -198,25 +185,4 @@
}
#endif // END AUTOMATICALLY_RESIZE
- (void)stop {
@synchronized(self) {
// If already stopping, do nothing.
if (!_continueRunLoop) {
return;
}
_continueRunLoop = NO;
}
if (_renderThread && _renderThread != [NSThread currentThread]) {
// Wait for the render thread to finish its loop and signal the semaphore.
// We use a 1-second timeout to prevent the app from hanging indefinitely
// if the thread gets stuck for some reason.
long timeoutResult = dispatch_semaphore_wait(_renderThreadSemaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (timeoutResult != 0) {
Log(LOG_E, @"MetalView render thread failed to stop gracefully within 1 second.");
}
}
_renderThread = nil;
}
@end
-4
View File
@@ -21,8 +21,4 @@
enableHdr:(BOOL)enableHdr
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
- (void)stop;
- (void)pause;
- (void)resume;
@end
+22 -72
View File
@@ -11,15 +11,12 @@ The implementation of the cross-platform game view controller.
#import "MetalVideoRenderer.h"
@implementation MetalViewController {
/// A queue to initialize the renderer asynchronously from the main thread.
dispatch_queue_t _dispatch_queue;
FrameQueue *_frameQueue;
float _framerate;
BOOL _enableHdr;
MetalView *_metalView;
MetalVideoRenderer *_renderer;
MetricsHandler _metricsHandler;
BOOL _stopping;
}
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate enableHdr:(BOOL)enableHdr metricsHandler:(MetricsHandler)metricsHandler {
@@ -30,8 +27,6 @@ The implementation of the cross-platform game view controller.
_framerate = framerate;
_enableHdr = enableHdr;
_metricsHandler = metricsHandler;
_stopping = NO;
[_frameQueue clear];
}
return self;
}
@@ -43,9 +38,6 @@ The implementation of the cross-platform game view controller.
- (void)viewDidLoad {
[super viewDidLoad];
/// A queue to initialize the renderer asynchronously from the main thread.
_dispatch_queue = dispatch_queue_create("com.moonlight.Metal", DISPATCH_QUEUE_CONCURRENT);
__block MetalView *view = (MetalView *)self.view;
if (!view) {
Log(LOG_E, @"The view attached to MetalViewController isn't a MetalView.");
@@ -75,34 +67,27 @@ The implementation of the cross-platform game view controller.
// Initialize the renderer-dependent view properties.
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
view.metalLayer.colorspace = renderer.colorspace;
view.metalLayer.maximumDrawableCount = 3;
self->_renderer = renderer;
}
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
if (!_stopping) {
// Renderer obtains a nextDrawable, waiting if necessary
[_renderer waitToRenderTo:layer];
// Renderer obtains a nextDrawable, waiting if necessary
[_renderer waitToRenderTo:layer];
// If we don't have a frame yet, wait on that too
if (!_stopping) {
[_frameQueue waitForEnqueue];
}
}
// If we don't have a frame yet, wait on that too
[_frameQueue waitForEnqueue];
}
/// Draw frame (used by manual loop)
- (void)renderTo:(nonnull CAMetalLayer *)layer {
if (!_renderer) {
return;
}
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
if (frame) {
[_renderer renderFrame:frame toLayer:layer];
if (!_renderer.isStopping) {
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
if (frame) {
[_renderer renderFrame:frame toLayer:layer];
}
}
}
@@ -110,6 +95,18 @@ The implementation of the cross-platform game view controller.
[_renderer drawableResize:size];
}
- (void)shutdown {
[_renderer shutdown];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
Log(LOG_I, @"XXX MetalViewController viewDidDisappear");
[_metalView shutdown];
}
#if TARGET_OS_IOS
/// Hides the Home indicator button automatically.
- (BOOL)prefersHomeIndicatorAutoHidden {
@@ -140,51 +137,4 @@ The implementation of the cross-platform game view controller.
}
#endif
- (void)stop {
// 1. Signal that we are stopping to prevent new work from starting.
_stopping = YES;
// 2. Invalidate the CAMetalLayer by removing its device.
// This should cause any blocking calls like `nextDrawable` on the render thread to fail
// and return immediately, breaking the deadlock.
if (_metalView) {
_metalView.metalLayer.device = nil;
}
// 3. Unblock the render thread from any other potential waiting points.
if (_renderer) {
[_renderer stop];
}
[_frameQueue stop];
// 4. Now that the thread is unblocked, wait for it to finish its execution.
if (_metalView) {
[_metalView stop];
}
// 5. Once the thread has terminated, it's safe to deallocate all resources.
_renderer = nil;
_metalView = nil;
}
- (void)pause {
if (_renderer) {
// Add this line to discard any stale drawable before pausing the thread.
// This forces the renderer to get a fresh one on resume.
[_renderer discardNextDrawable];
}
if (_metalView) {
[_metalView pause];
Log(LOG_I, @"Metal rendering paused.");
}
}
- (void)resume {
if (_metalView) {
[_metalView resume];
Log(LOG_I, @"Metal rendering resumed.");
}
}
@end
+84
View File
@@ -0,0 +1,84 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Vertex
{
float4 position [[ position ]];
float2 texCoords;
};
struct CscParams
{
float3 matrix[3];
float3 offsets;
};
// PQ (SMPTE ST 2084) constants for inverse EOTF
constant float PQ_M1 = 0.1593017578125; // 2610/16384
constant float PQ_M2 = 78.84375; // 2523/32 * 1000/1000
constant float PQ_C1 = 0.8359375; // 3424/4096
constant float PQ_C2 = 18.8515625; // 2413/128
constant float PQ_C3 = 18.6875; // 2392/128
// BT.2020 to Rec.709/sRGB color space conversion matrix
constant float3x3 bt2020_to_rec709 = float3x3(
float3( 1.7166511, -0.3556708, -0.2533663),
float3(-0.6666844, 1.6164812, 0.0157685),
float3( 0.0176399, -0.0427706, 0.9421031)
);
constexpr sampler s(coord::normalized, address::clamp_to_edge, filter::linear);
// Convert from PQ curve to linear light
float pq_to_linear(float pq) {
if (pq <= 0.0) return 0.0;
float pq_pow_inv_m2 = pow(pq, 1.0 / PQ_M2);
float numerator = max(pq_pow_inv_m2 - PQ_C1, 0.0);
float denominator = PQ_C2 - PQ_C3 * pq_pow_inv_m2;
if (denominator <= 0.0) return 0.0;
return pow(numerator / denominator, 1.0 / PQ_M1);
}
// Apply PQ inverse EOTF to RGB components
float3 pq_to_linear_rgb(float3 pq_rgb) {
return float3(
pq_to_linear(pq_rgb.r),
pq_to_linear(pq_rgb.g),
pq_to_linear(pq_rgb.b)
);
}
fragment float4 yuvToLinear(Vertex v [[ stage_in ]],
constant CscParams &cscParams [[ buffer(0) ]],
texture2d<float> luminancePlane [[ texture(0) ]],
texture2d<float> chrominancePlane [[ texture(1) ]])
{
float3 yuv = float3(luminancePlane.sample(s, v.texCoords).r,
chrominancePlane.sample(s, v.texCoords).rg);
yuv -= cscParams.offsets;
float3 rgb;
rgb.r = dot(yuv, cscParams.matrix[0]);
rgb.g = dot(yuv, cscParams.matrix[1]);
rgb.b = dot(yuv, cscParams.matrix[2]);
// Clamp RGB to valid range [0, 1]
rgb = clamp(rgb, 0.0, 1.0);
// Apply PQ inverse EOTF to convert from gamma-encoded to linear light
// This converts from 0-1 PQ range to 0-10000 nits linear
float3 linear_rgb = pq_to_linear_rgb(rgb);
// Scale for EDR (1.0 = 100 nits SDR white)
linear_rgb = linear_rgb * 100.0;
// TODO: support tonemapping to Rec.709 for non-HDR viewers
//linear_rgb = bt2020_to_rec709 * linear_rgb;
return float4(linear_rgb, 1.0f);
}
-6
View File
@@ -119,9 +119,3 @@ fragment float4 ps_draw_triplanar_10bit(Vertex v [[ stage_in ]],
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
return float4(rgb, 1.0f);
}
fragment float4 ps_draw_rgb(Vertex v [[ stage_in ]],
texture2d<float> rgbTexture [[ texture(0) ]])
{
return rgbTexture.sample(s, v.texCoords);
}
+5 -5
View File
@@ -192,15 +192,15 @@
double avgVideoMbps = [_connection getBwTracker].averageMbps;
double peakVideoMbps = [_connection getBwTracker].peakMbps;
if(overlayLevel == 1) return [LocalizationHelper localizedStringForKey:@"FPS: %5.2f Network dropped frames: %.2f%% Network latency: %@",
stats.totalFrames / interval,
stats.networkDroppedFrames / interval,
latencyString];
else return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@)\n"
"Bitrate: %.1f Mbps, Peak: %.1f, Renderer: %@\n"
"Bitrate: %.1f Mbps, Peak: %.1f, Frames buffered: %.1f\n"
"%@"
"Frames buffered: %.1f, Present \n"
"Renderer: %@\n"
"Frames dropped by network/pacing jitter: %.1f%% / %.1f%%\n"
"Average network latency: %@\n"
"Decode time: %.2f/%.2f/%.2f ms",
@@ -208,9 +208,9 @@
_config.height,
fps,
[_connection getActiveCodecName],
avgVideoMbps, peakVideoMbps, (stats.renderingBackend == RENDER_METAL) ? @"Metal" : @"AVSampleBuffer",
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
hostProcessingString,
stats.frameQueueMetrics.avg,
stats.renderingBackendString,
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
latencyString,
+45 -3
View File
@@ -201,7 +201,11 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
}
#endif
NSDictionary *destinationPixelBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat};
NSDictionary *destinationPixelBufferAttributes = @{
(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat,
(id)kVTDecompressionPropertyKey_GeneratePerFrameHDRDisplayMetadata : @YES,
(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder : @YES
};
return [self setupDecompressionSessionWithAttributes:destinationPixelBufferAttributes];
}
@@ -344,6 +348,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
}
- (void)cleanup
{
[_frameQueue shutdown];
if (_renderingBackend == RENDER_AVSB) {
[_displayLink invalidate];
}
@@ -607,7 +613,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
formatDesc = NULL;
}
Log(LOG_I, @"AV1 extensions: %@, format description: %@", extensions, formatDesc);
LogOnce(LOG_I, @"AV1 extensions: %@", extensions);
LogOnce(LOG_I, @"AV1 format description: %@", formatDesc);
ff_cbs_fragment_free(&cbsFrag);
ff_cbs_close(&cbsCtx);
@@ -674,6 +681,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
_formatDesc = NULL;
}
LogOnce(LOG_I, @"H264 format description: %@", _formatDesc);
// Free parameter set buffers after submission
[_parameterSetBuffers removeAllObjects];
}
@@ -713,6 +722,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
_formatDesc = NULL;
}
LogOnce(LOG_I, @"HEVC format description: %@", _formatDesc);
// Free parameter set buffers after submission
[_parameterSetBuffers removeAllObjects];
}
@@ -1003,8 +1014,39 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
}
- (void)getAllStats:(video_stats_t *)stats {
if (_renderingBackend == RENDER_METAL) {
#if TARGET_OS_OSX
float edrHeadroom = [[NSScreen mainScreen] maximumExtendedDynamicRangeColorComponentValue];
#else
float edrHeadroom = [[UIScreen mainScreen] currentEDRHeadroom];
UIScreenReferenceDisplayModeStatus referenceStatus = [[UIScreen mainScreen] referenceDisplayModeStatus];
#endif
if (edrHeadroom > 1.0) {
NSString *ref;
// Device has a reference display that may or may not be enabled
switch (referenceStatus) {
case UIScreenReferenceDisplayModeStatusLimited:
ref = @"(Reference mode limited),";
break;
case UIScreenReferenceDisplayModeStatusEnabled:
ref = @"(Reference mode),";
break;
default:
ref = @",";
break;
}
int peakNits = 1000;
stats->renderingBackendString = [NSString stringWithFormat:@"Metal, EDR %.1f %@ tone-mapped: %d nits",
edrHeadroom, ref, peakNits];
} else {
// if HDR
stats->renderingBackendString = [NSString stringWithFormat:@"Metal, tone-mapped: HDR->sRGB"];
}
} else {
stats->renderingBackendString = @"AVSampleBuffer";
}
dispatch_sync(_sq, ^{
stats->renderingBackend = _renderingBackend;
memcpy(&stats->decodeMetrics, &_decodeMetrics, sizeof(PlotMetrics));
memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics));
[_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics];
+1 -1
View File
@@ -36,7 +36,7 @@
_count = 0;
_minValue = 0.0f;
_maxValue = 0.0f;
_sum = 0.0;
_sum = 0.0f;
_sq = dispatch_queue_create("com.floatbuffer.serial", DISPATCH_QUEUE_SERIAL);
}
+2 -1
View File
@@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) FloatBuffer *frameDropMetrics;
@property (nonatomic) int highWaterMark;
@property (nonatomic, readonly) int maxCapacity;
@property (atomic) BOOL isStopping;
+ (instancetype)sharedInstance;
@@ -26,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
- (CFTimeInterval)estimatedFramerate;
- (int)currentSoftCap;
- (void)waitForEnqueue;
- (void)stop;
- (void)shutdown;
@end
+15 -47
View File
@@ -16,7 +16,6 @@
int _count;
BOOL _droppedLast;
BOOL _stopping;
int _framesIn;
CMTime _ptsCorrection;
os_unfair_lock _lock;
@@ -61,6 +60,7 @@
_ptsCorrection = CMTimeMake(0, 90000);
_queueSizeHistory = [[FloatBuffer alloc] initWithCapacity:64];
_lock = OS_UNFAIR_LOCK_INIT;
_isStopping = NO;
// ring buffer
_capacity = _maxCapacity;
@@ -76,7 +76,6 @@
// ping estimatedFramerate to set initial last value
[self estimatedFramerate];
_stopping = NO;
}
return self;
}
@@ -173,10 +172,6 @@
// enqueue with simple alternate-drop logic
- (int)enqueue:(Frame *)frame {
os_unfair_lock_lock(&_lock);
if (_stopping) {
os_unfair_lock_unlock(&_lock);
return 0; // Don't enqueue if stopping
}
int dropCount = [self _unsafeEnqueue:frame withDropTarget:_highWaterMark];
os_unfair_lock_unlock(&_lock);
return dropCount;
@@ -185,10 +180,6 @@
// enqueue that is a bit more flexixble, using the same 500ms queue size history method as moonlight-qt.
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack {
os_unfair_lock_lock(&_lock);
if (_stopping) {
os_unfair_lock_unlock(&_lock);
return 0; // Don't enqueue if stopping
}
CFTimeInterval now = CACurrentMediaTime();
// new data point for queue health
@@ -235,14 +226,9 @@
// Allows the render loop to wait if the queue is empty
- (void)waitForEnqueue {
while ([self isEmpty]) {
if (_stopping) {
return;
}
dispatch_semaphore_wait(_frameSemaphore, DISPATCH_TIME_FOREVER);
if (_stopping) {
return;
}
while (!self.isStopping && [self isEmpty]) {
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
dispatch_semaphore_wait(_frameSemaphore, timeout);
}
}
@@ -270,11 +256,12 @@
CFTimeInterval deadline = start + timeout;
int round = 0;
if (self.isStopping) {
return nil;
}
// Always attempt to dequeue at least once
do {
if (_stopping) {
return nil;
}
if (round > 0) {
usleep(100); // 0.1ms
}
@@ -304,7 +291,6 @@
os_unfair_lock_lock(&_lock);
_head = _tail = _count = 0;
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
_stopping = NO;
os_unfair_lock_unlock(&_lock);
}
@@ -338,6 +324,13 @@
return cap;
}
- (void)shutdown {
// new frames will no longer be coming in, make sure consumer side is not left waiting
self.isStopping = YES;
Log(LOG_I, @"XXX FrameQueue shutting down");
dispatch_semaphore_signal(_frameSemaphore);
}
// For use with NSLog("%@", franeQueue);
- (NSString *)description {
__block NSMutableArray *parts = [NSMutableArray arrayWithCapacity:_count];
@@ -347,29 +340,4 @@
return [NSString stringWithFormat:@"[%@]", [parts componentsJoinedByString:@",\n"]];
}
- (void)stop {
os_unfair_lock_lock(&_lock);
// Prevent redundant calls
if (_stopping) {
os_unfair_lock_unlock(&_lock);
return;
}
_stopping = YES;
// Clear the buffer's contents
_head = _tail = _count = 0;
for (int i = 0; i < _capacity; i++) {
[_buffer replaceObjectAtIndex:i withObject:[NSNull null]];
}
os_unfair_lock_unlock(&_lock);
// Unblock any threads waiting on the semaphore.
// Signaling multiple times ensures all potential consumer threads are released.
for (int i = 0; i < _maxCapacity; i++) {
dispatch_semaphore_signal(_frameSemaphore);
}
}
@end
+3 -1
View File
@@ -11,7 +11,9 @@
#if defined(NDEBUG)
static LogLevel LoggerLogLevel = LOG_I;
#else
static LogLevel LoggerLogLevel = LOG_D;
// Debug is too spammy during discovery
// static LogLevel LoggerLogLevel = LOG_D;
static LogLevel LoggerLogLevel = LOG_I;
#endif
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
+1 -1
View File
@@ -52,5 +52,5 @@ typedef struct {
PlotMetrics decodeMetrics;
PlotMetrics frameQueueMetrics;
PlotMetrics frameDropMetrics;
RenderingBackend renderingBackend;
NSString *renderingBackendString;
} video_stats_t;
@@ -745,7 +745,6 @@
_inactivityTimer = nil;
}
if (self.metalViewController) {
[self.metalViewController stop];
[self.metalViewController.view removeFromSuperview];
self.metalViewController = nil;
NSLog(@"Metal renderer stopped and cleaned up.");
@@ -926,11 +925,6 @@
// This will fire if the user opens control center or gets a low battery message
- (void)applicationWillResignActive:(NSNotification *)notification {
if (self.metalViewController) {
[self.metalViewController pause];
}
//[self.pipController startPictureInPicture];
//sleep(1);
@@ -947,9 +941,6 @@
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
if (self.metalViewController) {
[self.metalViewController resume];
}
// Stop the background timer, since we're foregrounded again
if (_inactivityTimer != nil) {
Log(LOG_I, @"Stopping inactivity timer after becoming active again");