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:
1 parent
034ee52eec
commit
1e24731b60
17 files changed
+389
-329
No files matched your search
@@ -40,7 +40,7 @@
|
|||||||
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; };
|
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; };
|
||||||
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
|
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
|
||||||
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */; };
|
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 */; };
|
97E20DCF2E21647A000D5DCA /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCD2E21647A000D5DCA /* Shaders.metal */; };
|
||||||
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCC2E21647A000D5DCA /* MetalViewController.m */; };
|
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCC2E21647A000D5DCA /* MetalViewController.m */; };
|
||||||
97E20DD12E21647A000D5DCA /* MetalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E20DCA2E21647A000D5DCA /* MetalView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
97E20DC62E21647A000D5DCA /* MetalConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MetalConfig.h; sourceTree = "<group>"; };
|
||||||
@@ -469,6 +470,7 @@
|
|||||||
97E20DCE2E21647A000D5DCA /* Metal */ = {
|
97E20DCE2E21647A000D5DCA /* Metal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */,
|
||||||
97E20DC62E21647A000D5DCA /* MetalConfig.h */,
|
97E20DC62E21647A000D5DCA /* MetalConfig.h */,
|
||||||
97E20DC72E21647A000D5DCA /* MetalVideoRenderer.h */,
|
97E20DC72E21647A000D5DCA /* MetalVideoRenderer.h */,
|
||||||
97E20DC82E21647A000D5DCA /* MetalVideoRenderer.m */,
|
97E20DC82E21647A000D5DCA /* MetalVideoRenderer.m */,
|
||||||
@@ -1022,6 +1024,7 @@
|
|||||||
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
||||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
|
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
|
||||||
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
||||||
|
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
||||||
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
||||||
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
||||||
FB1D59971BBCCB6400F482CA /* ComputerScrollView.m in Sources */,
|
FB1D59971BBCCB6400F482CA /* ComputerScrollView.m in Sources */,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#import <QuartzCore/CAMetalLayer.h>
|
#import <QuartzCore/CAMetalLayer.h>
|
||||||
#import <QuartzCore/QuartzCore.h>
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
#import <simd/simd.h>
|
||||||
#import "ConnectionCallbacks.h"
|
#import "ConnectionCallbacks.h"
|
||||||
#import "Frame.h"
|
#import "Frame.h"
|
||||||
#import "Plot.h"
|
#import "Plot.h"
|
||||||
@@ -9,17 +10,14 @@
|
|||||||
@property (atomic) CFTimeInterval averageGPUTime;
|
@property (atomic) CFTimeInterval averageGPUTime;
|
||||||
@property (nonatomic) NSUInteger sampleCount;
|
@property (nonatomic) NSUInteger sampleCount;
|
||||||
@property (nonatomic) MTLPixelFormat colorPixelFormat;
|
@property (nonatomic) MTLPixelFormat colorPixelFormat;
|
||||||
@property (nonatomic, nonnull) CGColorSpaceRef colorspace;
|
@property (nonatomic) CFTimeInterval lastPresented;
|
||||||
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
||||||
|
@property (atomic) BOOL isStopping;
|
||||||
|
|
||||||
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device
|
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate;
|
||||||
drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat
|
|
||||||
framerate:(float)framerate;
|
|
||||||
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
|
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||||
- (void)drawableResize:(CGSize)drawableSize;
|
- (void)drawableResize:(CGSize)drawableSize;
|
||||||
- (void)plotFrametime:(CFTimeInterval)presentedTime;
|
- (void)shutdown;
|
||||||
- (void)stop;
|
|
||||||
- (void)discardNextDrawable;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
#import "MetalVideoRenderer.h"
|
#import "MetalVideoRenderer.h"
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <CoreVideo/CoreVideo.h>
|
#import <CoreVideo/CoreVideo.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
#import <Metal/Metal.h>
|
#import <Metal/Metal.h>
|
||||||
#import <MetalKit/MetalKit.h>
|
#import <MetalKit/MetalKit.h>
|
||||||
|
#import <simd/simd.h>
|
||||||
#import "ImGuiPlots.h"
|
#import "ImGuiPlots.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
@@ -81,7 +84,6 @@ static const struct CscParams k_CscParams_Bt2020Full_10bit = {
|
|||||||
{1.0f, -0.1646f, -0.5714f},
|
{1.0f, -0.1646f, -0.5714f},
|
||||||
{1.0f, 1.8814f, 0.0f},
|
{1.0f, 1.8814f, 0.0f},
|
||||||
},
|
},
|
||||||
|
|
||||||
{0.0f, 512.0f / 1023.0f, 512.0f / 1023.0f},
|
{0.0f, 512.0f / 1023.0f, 512.0f / 1023.0f},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,10 +103,10 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
id<MTLLibrary> _shaderLibrary;
|
id<MTLLibrary> _shaderLibrary;
|
||||||
id<MTLRenderPipelineState> _videoPipelineState[MAX_VIDEO_PLANES];
|
id<MTLRenderPipelineState> _videoPipelineState[MAX_VIDEO_PLANES];
|
||||||
MTLRenderPassDescriptor *_renderPassDescriptor;
|
MTLRenderPassDescriptor *_renderPassDescriptor;
|
||||||
id<MTLTexture> _videoTexture;
|
|
||||||
CVMetalTextureCacheRef _textureCache;
|
CVMetalTextureCacheRef _textureCache;
|
||||||
CVMetalTextureRef _cvMetalTextures[MAX_VIDEO_PLANES];
|
CVMetalTextureRef _cvMetalTextures[MAX_VIDEO_PLANES];
|
||||||
|
|
||||||
|
CGFloat _currentEDRHeadroom;
|
||||||
int _lastColorSpace;
|
int _lastColorSpace;
|
||||||
BOOL _lastFullRange;
|
BOOL _lastFullRange;
|
||||||
size_t _lastFrameWidth;
|
size_t _lastFrameWidth;
|
||||||
@@ -113,8 +115,9 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
size_t _lastDrawableHeight;
|
size_t _lastDrawableHeight;
|
||||||
id<MTLBuffer> _CscParamsBuffer;
|
id<MTLBuffer> _CscParamsBuffer;
|
||||||
id<MTLBuffer> _VideoVertexBuffer;
|
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 {
|
- (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));
|
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||||
_averageGPUTime = (1.0f / framerate) / 2;
|
_averageGPUTime = (1.0f / framerate) / 2;
|
||||||
_device = device;
|
_device = device;
|
||||||
_nextDrawable = nil;
|
|
||||||
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||||
_colorspace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
|
|
||||||
_framerate = framerate;
|
_framerate = framerate;
|
||||||
_commandQueue = [_device newCommandQueue];
|
_commandQueue = [_device newCommandQueue];
|
||||||
|
_currentEDRHeadroom = 1.0f;
|
||||||
_lastColorSpace = -1;
|
_lastColorSpace = -1;
|
||||||
_lastFullRange = NO;
|
_lastFullRange = NO;
|
||||||
_lastPresented = 0;
|
_lastPresented = 0.0f;
|
||||||
|
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
||||||
|
_isStopping = NO;
|
||||||
|
|
||||||
CFStringRef keys[1] = {kCVMetalTextureUsage};
|
CFStringRef keys[1] = {kCVMetalTextureUsage};
|
||||||
NSUInteger values[1] = {MTLTextureUsageShaderRead};
|
NSUInteger values[1] = {MTLTextureUsageShaderRead};
|
||||||
@@ -148,63 +152,94 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
// Release the Core Foundation texture cache, which is not managed by ARC.
|
if (_CscParamsBuffer) {
|
||||||
if (_textureCache) {
|
_CscParamsBuffer = nil;
|
||||||
CFRelease(_textureCache);
|
}
|
||||||
_textureCache = NULL;
|
if (_VideoVertexBuffer) {
|
||||||
|
_VideoVertexBuffer = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safely release any textures that might still be referenced.
|
|
||||||
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
|
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
|
||||||
if (_cvMetalTextures[i]) {
|
if (_videoPipelineState[i]) {
|
||||||
CFRelease(_cvMetalTextures[i]);
|
_videoPipelineState[i] = nil;
|
||||||
_cvMetalTextures[i] = NULL;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_renderPassDescriptor) {
|
||||||
// ARC will handle the rest of the Objective-C objects like _CscParamsBuffer.
|
_renderPassDescriptor = nil;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
- (void)applyEDRFromFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
- (void)reportMaxEDRHeadroom {
|
||||||
CFDictionaryRef ext = [frame getFormatDescExtensions];
|
CGFloat maxHeadroom = 1.0f;
|
||||||
|
#if TARGET_OS_OSX
|
||||||
Log(LOG_I, @"ext: %@", ext);
|
maxHeadroom = [[NSScreen mainScreen] maximumPotentialExtendedDynamicRangeColorComponentValue];
|
||||||
|
#else
|
||||||
CFDataRef masteringData = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_MasteringDisplayColorVolume);
|
maxHeadroom = [[UIScreen mainScreen] potentialEDRHeadroom];
|
||||||
CFDataRef contentDataRef = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_ContentLightLevelInfo);
|
#endif
|
||||||
|
if (maxHeadroom > 1.0) {
|
||||||
if (masteringData) {
|
LogOnce(LOG_I, @"Display supports EDR with a max headroom of %.1f", maxHeadroom);
|
||||||
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);
|
|
||||||
} else {
|
} 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.wantsExtendedDynamicRangeContent = YES;
|
||||||
layer.pixelFormat = MTLPixelFormatRGBA16Float;
|
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);
|
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
|
||||||
layer.colorspace = colorspace;
|
layer.colorspace = colorspace;
|
||||||
|
CGColorSpaceRelease(colorspace);
|
||||||
|
|
||||||
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0.0005f maxLuminance:1000.0f opticalOutputScale:100.0f];
|
CFDataRef masteringDisplayColorVolume = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferMasteringDisplayColorVolumeKey, nil);
|
||||||
|
CFDataRef contentLightLevel = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferContentLightLevelInfoKey, nil);
|
||||||
Log(LOG_I, @"EDRMetadata set for 1000 nits");
|
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
|
#endif
|
||||||
|
|
||||||
@@ -293,6 +328,9 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
|
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([CAEDRMetadata isAvailable]) {
|
||||||
|
[self applyEDRFromFrame:frame withColorspace:colorspace toLayer:layer];
|
||||||
|
} else {
|
||||||
// These can only be changed on the main thread
|
// These can only be changed on the main thread
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
@@ -305,6 +343,7 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
});
|
});
|
||||||
CGColorSpaceRelease(newColorSpace);
|
CGColorSpaceRelease(newColorSpace);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the new colorspace parameter buffer for our fragment shader
|
// Create the new colorspace parameter buffer for our fragment shader
|
||||||
MTLResourceOptions bufferOptions = MTLResourceStorageModeShared;
|
MTLResourceOptions bufferOptions = MTLResourceStorageModeShared;
|
||||||
@@ -383,12 +422,13 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)discardNextDrawable {
|
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||||
_nextDrawable = nil;
|
@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
|
// Handle changes to the frame's colorspace from last time we rendered
|
||||||
BOOL layerDidChange = NO;
|
BOOL layerDidChange = NO;
|
||||||
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
|
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);
|
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);
|
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||||
assert(planes <= MAX_VIDEO_PLANES);
|
assert(planes <= MAX_VIDEO_PLANES);
|
||||||
|
|
||||||
@@ -419,11 +454,15 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
if (!_videoPipelineState[planes]) {
|
if (!_videoPipelineState[planes]) {
|
||||||
MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
|
MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
|
||||||
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
|
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)
|
// Determine if this is 10-bit based on the layer's pixel format (after colorspace update)
|
||||||
BOOL is10Bit = (layer.pixelFormat == MTLPixelFormatBGR10A2Unorm);
|
BOOL is10Bit = (layer.pixelFormat == MTLPixelFormatBGR10A2Unorm);
|
||||||
Log(LOG_I, @"Layer pixel format: %lu, is10Bit: %@", (unsigned long)layer.pixelFormat, is10Bit ? @"YES" : @"NO");
|
|
||||||
|
|
||||||
NSString *fragmentShaderName;
|
NSString *fragmentShaderName;
|
||||||
if (planes == 2) {
|
if (planes == 2) {
|
||||||
@@ -432,11 +471,22 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
fragmentShaderName = is10Bit ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
|
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.colorAttachments[0].pixelFormat = layer.pixelFormat;
|
||||||
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
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;
|
NSError *error = nil;
|
||||||
_videoPipelineState[planes] = [_device newRenderPipelineStateWithDescriptor:pipelineDesc error:&error];
|
_videoPipelineState[planes] = [_device newRenderPipelineStateWithDescriptor:pipelineDesc error:&error];
|
||||||
if (!_videoPipelineState[planes]) {
|
if (!_videoPipelineState[planes]) {
|
||||||
@@ -468,9 +518,6 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cvMetalTextures[i]) {
|
|
||||||
CVBufferRelease(_cvMetalTextures[i]);
|
|
||||||
}
|
|
||||||
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
||||||
_textureCache,
|
_textureCache,
|
||||||
frame.pixelBuffer,
|
frame.pixelBuffer,
|
||||||
@@ -501,11 +548,45 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
|
[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 setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
|
||||||
|
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
|
||||||
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
||||||
[renderEncoder endEncoding];
|
[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
|
#if TARGET_OS_SIMULATOR
|
||||||
[commandBuffer presentDrawable:drawable];
|
[commandBuffer presentDrawable:drawable];
|
||||||
#else
|
#else
|
||||||
@@ -515,39 +596,28 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
|
|
||||||
[commandBuffer commit];
|
[commandBuffer commit];
|
||||||
|
|
||||||
__weak typeof(self) self_ = self;
|
// Wait for the command buffer to complete and free our CVMetalTextureCache references
|
||||||
[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);
|
|
||||||
[commandBuffer waitUntilCompleted];
|
[commandBuffer waitUntilCompleted];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)plotFrametime:(CFTimeInterval)presentedTime {
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||||
if (_lastPresented > 0) {
|
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
|
||||||
CFTimeInterval frametime = presentedTime - _lastPresented;
|
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
|
||||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
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.
|
/// Responds to the drawable's size or orientation changes.
|
||||||
@@ -558,7 +628,4 @@ static const NSUInteger MaxFramesInFlight = 3;
|
|||||||
- (void)resize:(CGSize)size {
|
- (void)resize:(CGSize)size {
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)stop {
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
- (void)drawableResize:(CGSize)size;
|
- (void)drawableResize:(CGSize)size;
|
||||||
- (void)renderTo:(nonnull CAMetalLayer *)layer;
|
- (void)renderTo:(nonnull CAMetalLayer *)layer;
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||||
|
- (void)shutdown;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -29,12 +30,9 @@
|
|||||||
@property (nonatomic) float framerate;
|
@property (nonatomic) float framerate;
|
||||||
|
|
||||||
- (void)initCommon;
|
- (void)initCommon;
|
||||||
|
- (void)shutdown;
|
||||||
#if AUTOMATICALLY_RESIZE
|
#if AUTOMATICALLY_RESIZE
|
||||||
- (void)resizeDrawable:(CGFloat)scaleFactor;
|
- (void)resizeDrawable:(CGFloat)scaleFactor;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
- (void)stop;
|
|
||||||
- (void)pause;
|
|
||||||
- (void)resume;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
+46
-80
@@ -8,10 +8,6 @@
|
|||||||
@implementation MetalView {
|
@implementation MetalView {
|
||||||
// The secondary thread containing the render loop.
|
// The secondary thread containing the render loop.
|
||||||
NSThread *_renderThread;
|
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.
|
#pragma mark - Initialization and Setup.
|
||||||
@@ -42,7 +38,17 @@
|
|||||||
_metalLayer = (CAMetalLayer *)self.layer;
|
_metalLayer = (CAMetalLayer *)self.layer;
|
||||||
|
|
||||||
self.layer.delegate = self;
|
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
|
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||||
@@ -63,71 +69,52 @@
|
|||||||
}
|
}
|
||||||
#endif // END TARGET_OS_IOS || TARGET_OS_TV
|
#endif // END TARGET_OS_IOS || TARGET_OS_TV
|
||||||
|
|
||||||
- (void)startRenderThread {
|
- (void)movedToWindow {
|
||||||
@synchronized(self) {
|
if (!self.window) {
|
||||||
// Don't start a new thread if one is already running
|
return;
|
||||||
|
|
||||||
|
// We have been removed
|
||||||
if (_renderThread) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_continueRunLoop = YES;
|
// Render on a new thread
|
||||||
_renderThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
|
_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.qualityOfService = NSQualityOfServiceUserInteractive;
|
||||||
[_renderThread start];
|
[_renderThread start];
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)movedToWindow {
|
// Perform any actions that need to know the size and scale of the drawable. When UIKit calls
|
||||||
if (self.window) {
|
// didMoveToWindow after the view initialization, this is the first opportunity to notify
|
||||||
// The view was added to a window, so start rendering.
|
// components of the drawable's size.
|
||||||
[self resume];
|
#if AUTOMATICALLY_RESIZE
|
||||||
|
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||||
// Notify the delegate of the drawable's size.
|
[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;
|
CGSize defaultDrawableSize = self.bounds.size;
|
||||||
defaultDrawableSize.width *= self.layer.contentsScale;
|
defaultDrawableSize.width *= self.layer.contentsScale;
|
||||||
defaultDrawableSize.height *= self.layer.contentsScale;
|
defaultDrawableSize.height *= self.layer.contentsScale;
|
||||||
[self.delegate drawableResize:defaultDrawableSize];
|
[self.delegate drawableResize:defaultDrawableSize];
|
||||||
} else {
|
#endif
|
||||||
// The view was removed from a window, so stop rendering.
|
|
||||||
[self pause];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch_semaphore_signal(self->_renderThreadSemaphore);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Resizing
|
#pragma mark - Resizing
|
||||||
@@ -198,25 +185,4 @@
|
|||||||
}
|
}
|
||||||
#endif // END AUTOMATICALLY_RESIZE
|
#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
|
@end
|
||||||
@@ -21,8 +21,4 @@
|
|||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||||
|
|
||||||
- (void)stop;
|
|
||||||
- (void)pause;
|
|
||||||
- (void)resume;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -11,15 +11,12 @@ The implementation of the cross-platform game view controller.
|
|||||||
#import "MetalVideoRenderer.h"
|
#import "MetalVideoRenderer.h"
|
||||||
|
|
||||||
@implementation MetalViewController {
|
@implementation MetalViewController {
|
||||||
/// A queue to initialize the renderer asynchronously from the main thread.
|
|
||||||
dispatch_queue_t _dispatch_queue;
|
|
||||||
FrameQueue *_frameQueue;
|
FrameQueue *_frameQueue;
|
||||||
float _framerate;
|
float _framerate;
|
||||||
BOOL _enableHdr;
|
BOOL _enableHdr;
|
||||||
MetalView *_metalView;
|
MetalView *_metalView;
|
||||||
MetalVideoRenderer *_renderer;
|
MetalVideoRenderer *_renderer;
|
||||||
MetricsHandler _metricsHandler;
|
MetricsHandler _metricsHandler;
|
||||||
BOOL _stopping;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate enableHdr:(BOOL)enableHdr metricsHandler:(MetricsHandler)metricsHandler {
|
- (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;
|
_framerate = framerate;
|
||||||
_enableHdr = enableHdr;
|
_enableHdr = enableHdr;
|
||||||
_metricsHandler = metricsHandler;
|
_metricsHandler = metricsHandler;
|
||||||
_stopping = NO;
|
|
||||||
[_frameQueue clear];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -43,9 +38,6 @@ The implementation of the cross-platform game view controller.
|
|||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super 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;
|
__block MetalView *view = (MetalView *)self.view;
|
||||||
if (!view) {
|
if (!view) {
|
||||||
Log(LOG_E, @"The view attached to MetalViewController isn't a MetalView.");
|
Log(LOG_E, @"The view attached to MetalViewController isn't a MetalView.");
|
||||||
@@ -75,41 +67,46 @@ The implementation of the cross-platform game view controller.
|
|||||||
|
|
||||||
// Initialize the renderer-dependent view properties.
|
// Initialize the renderer-dependent view properties.
|
||||||
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
||||||
view.metalLayer.colorspace = renderer.colorspace;
|
|
||||||
view.metalLayer.maximumDrawableCount = 3;
|
view.metalLayer.maximumDrawableCount = 3;
|
||||||
|
|
||||||
self->_renderer = renderer;
|
self->_renderer = renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||||
if (!_stopping) {
|
|
||||||
// Renderer obtains a nextDrawable, waiting if necessary
|
// Renderer obtains a nextDrawable, waiting if necessary
|
||||||
[_renderer waitToRenderTo:layer];
|
[_renderer waitToRenderTo:layer];
|
||||||
|
|
||||||
// If we don't have a frame yet, wait on that too
|
// If we don't have a frame yet, wait on that too
|
||||||
if (!_stopping) {
|
|
||||||
[_frameQueue waitForEnqueue];
|
[_frameQueue waitForEnqueue];
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw frame (used by manual loop)
|
/// Draw frame (used by manual loop)
|
||||||
- (void)renderTo:(nonnull CAMetalLayer *)layer {
|
- (void)renderTo:(nonnull CAMetalLayer *)layer {
|
||||||
if (!_renderer) {
|
if (!_renderer.isStopping) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
||||||
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
||||||
if (frame) {
|
if (frame) {
|
||||||
[_renderer renderFrame:frame toLayer:layer];
|
[_renderer renderFrame:frame toLayer:layer];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)drawableResize:(CGSize)size {
|
- (void)drawableResize:(CGSize)size {
|
||||||
[_renderer drawableResize:size];
|
[_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
|
#if TARGET_OS_IOS
|
||||||
/// Hides the Home indicator button automatically.
|
/// Hides the Home indicator button automatically.
|
||||||
- (BOOL)prefersHomeIndicatorAutoHidden {
|
- (BOOL)prefersHomeIndicatorAutoHidden {
|
||||||
@@ -140,51 +137,4 @@ The implementation of the cross-platform game view controller.
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
@end
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -119,9 +119,3 @@ fragment float4 ps_draw_triplanar_10bit(Vertex v [[ stage_in ]],
|
|||||||
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
|
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
|
||||||
return float4(rgb, 1.0f);
|
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);
|
|
||||||
}
|
|
||||||
@@ -198,9 +198,9 @@
|
|||||||
stats.networkDroppedFrames / interval,
|
stats.networkDroppedFrames / interval,
|
||||||
latencyString];
|
latencyString];
|
||||||
else return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@)\n"
|
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"
|
"Frames dropped by network/pacing jitter: %.1f%% / %.1f%%\n"
|
||||||
"Average network latency: %@\n"
|
"Average network latency: %@\n"
|
||||||
"Decode time: %.2f/%.2f/%.2f ms",
|
"Decode time: %.2f/%.2f/%.2f ms",
|
||||||
@@ -208,9 +208,9 @@
|
|||||||
_config.height,
|
_config.height,
|
||||||
fps,
|
fps,
|
||||||
[_connection getActiveCodecName],
|
[_connection getActiveCodecName],
|
||||||
avgVideoMbps, peakVideoMbps, (stats.renderingBackend == RENDER_METAL) ? @"Metal" : @"AVSampleBuffer",
|
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
|
||||||
hostProcessingString,
|
hostProcessingString,
|
||||||
stats.frameQueueMetrics.avg,
|
stats.renderingBackendString,
|
||||||
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
|
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
|
||||||
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
|
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
|
||||||
latencyString,
|
latencyString,
|
||||||
|
|||||||
@@ -201,7 +201,11 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
NSDictionary *destinationPixelBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat};
|
NSDictionary *destinationPixelBufferAttributes = @{
|
||||||
|
(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat,
|
||||||
|
(id)kVTDecompressionPropertyKey_GeneratePerFrameHDRDisplayMetadata : @YES,
|
||||||
|
(id)kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder : @YES
|
||||||
|
};
|
||||||
|
|
||||||
return [self setupDecompressionSessionWithAttributes:destinationPixelBufferAttributes];
|
return [self setupDecompressionSessionWithAttributes:destinationPixelBufferAttributes];
|
||||||
}
|
}
|
||||||
@@ -344,6 +348,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
}
|
}
|
||||||
- (void)cleanup
|
- (void)cleanup
|
||||||
{
|
{
|
||||||
|
[_frameQueue shutdown];
|
||||||
|
|
||||||
if (_renderingBackend == RENDER_AVSB) {
|
if (_renderingBackend == RENDER_AVSB) {
|
||||||
[_displayLink invalidate];
|
[_displayLink invalidate];
|
||||||
}
|
}
|
||||||
@@ -607,7 +613,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
formatDesc = NULL;
|
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_fragment_free(&cbsFrag);
|
||||||
ff_cbs_close(&cbsCtx);
|
ff_cbs_close(&cbsCtx);
|
||||||
@@ -674,6 +681,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
_formatDesc = NULL;
|
_formatDesc = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogOnce(LOG_I, @"H264 format description: %@", _formatDesc);
|
||||||
|
|
||||||
// Free parameter set buffers after submission
|
// Free parameter set buffers after submission
|
||||||
[_parameterSetBuffers removeAllObjects];
|
[_parameterSetBuffers removeAllObjects];
|
||||||
}
|
}
|
||||||
@@ -713,6 +722,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
_formatDesc = NULL;
|
_formatDesc = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogOnce(LOG_I, @"HEVC format description: %@", _formatDesc);
|
||||||
|
|
||||||
// Free parameter set buffers after submission
|
// Free parameter set buffers after submission
|
||||||
[_parameterSetBuffers removeAllObjects];
|
[_parameterSetBuffers removeAllObjects];
|
||||||
}
|
}
|
||||||
@@ -1003,8 +1014,39 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)getAllStats:(video_stats_t *)stats {
|
- (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, ^{
|
dispatch_sync(_sq, ^{
|
||||||
stats->renderingBackend = _renderingBackend;
|
|
||||||
memcpy(&stats->decodeMetrics, &_decodeMetrics, sizeof(PlotMetrics));
|
memcpy(&stats->decodeMetrics, &_decodeMetrics, sizeof(PlotMetrics));
|
||||||
memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics));
|
memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics));
|
||||||
[_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics];
|
[_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics];
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
_count = 0;
|
_count = 0;
|
||||||
_minValue = 0.0f;
|
_minValue = 0.0f;
|
||||||
_maxValue = 0.0f;
|
_maxValue = 0.0f;
|
||||||
_sum = 0.0;
|
_sum = 0.0f;
|
||||||
|
|
||||||
_sq = dispatch_queue_create("com.floatbuffer.serial", DISPATCH_QUEUE_SERIAL);
|
_sq = dispatch_queue_create("com.floatbuffer.serial", DISPATCH_QUEUE_SERIAL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic) FloatBuffer *frameDropMetrics;
|
@property (nonatomic) FloatBuffer *frameDropMetrics;
|
||||||
@property (nonatomic) int highWaterMark;
|
@property (nonatomic) int highWaterMark;
|
||||||
@property (nonatomic, readonly) int maxCapacity;
|
@property (nonatomic, readonly) int maxCapacity;
|
||||||
|
@property (atomic) BOOL isStopping;
|
||||||
|
|
||||||
+ (instancetype)sharedInstance;
|
+ (instancetype)sharedInstance;
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
- (CFTimeInterval)estimatedFramerate;
|
- (CFTimeInterval)estimatedFramerate;
|
||||||
- (int)currentSoftCap;
|
- (int)currentSoftCap;
|
||||||
- (void)waitForEnqueue;
|
- (void)waitForEnqueue;
|
||||||
- (void)stop;
|
- (void)shutdown;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
int _count;
|
int _count;
|
||||||
|
|
||||||
BOOL _droppedLast;
|
BOOL _droppedLast;
|
||||||
BOOL _stopping;
|
|
||||||
int _framesIn;
|
int _framesIn;
|
||||||
CMTime _ptsCorrection;
|
CMTime _ptsCorrection;
|
||||||
os_unfair_lock _lock;
|
os_unfair_lock _lock;
|
||||||
@@ -61,6 +60,7 @@
|
|||||||
_ptsCorrection = CMTimeMake(0, 90000);
|
_ptsCorrection = CMTimeMake(0, 90000);
|
||||||
_queueSizeHistory = [[FloatBuffer alloc] initWithCapacity:64];
|
_queueSizeHistory = [[FloatBuffer alloc] initWithCapacity:64];
|
||||||
_lock = OS_UNFAIR_LOCK_INIT;
|
_lock = OS_UNFAIR_LOCK_INIT;
|
||||||
|
_isStopping = NO;
|
||||||
|
|
||||||
// ring buffer
|
// ring buffer
|
||||||
_capacity = _maxCapacity;
|
_capacity = _maxCapacity;
|
||||||
@@ -76,7 +76,6 @@
|
|||||||
|
|
||||||
// ping estimatedFramerate to set initial last value
|
// ping estimatedFramerate to set initial last value
|
||||||
[self estimatedFramerate];
|
[self estimatedFramerate];
|
||||||
_stopping = NO;
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -173,10 +172,6 @@
|
|||||||
// enqueue with simple alternate-drop logic
|
// enqueue with simple alternate-drop logic
|
||||||
- (int)enqueue:(Frame *)frame {
|
- (int)enqueue:(Frame *)frame {
|
||||||
os_unfair_lock_lock(&_lock);
|
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];
|
int dropCount = [self _unsafeEnqueue:frame withDropTarget:_highWaterMark];
|
||||||
os_unfair_lock_unlock(&_lock);
|
os_unfair_lock_unlock(&_lock);
|
||||||
return dropCount;
|
return dropCount;
|
||||||
@@ -185,10 +180,6 @@
|
|||||||
// enqueue that is a bit more flexixble, using the same 500ms queue size history method as moonlight-qt.
|
// 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 {
|
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack {
|
||||||
os_unfair_lock_lock(&_lock);
|
os_unfair_lock_lock(&_lock);
|
||||||
if (_stopping) {
|
|
||||||
os_unfair_lock_unlock(&_lock);
|
|
||||||
return 0; // Don't enqueue if stopping
|
|
||||||
}
|
|
||||||
CFTimeInterval now = CACurrentMediaTime();
|
CFTimeInterval now = CACurrentMediaTime();
|
||||||
|
|
||||||
// new data point for queue health
|
// new data point for queue health
|
||||||
@@ -235,14 +226,9 @@
|
|||||||
|
|
||||||
// Allows the render loop to wait if the queue is empty
|
// Allows the render loop to wait if the queue is empty
|
||||||
- (void)waitForEnqueue {
|
- (void)waitForEnqueue {
|
||||||
while ([self isEmpty]) {
|
while (!self.isStopping && [self isEmpty]) {
|
||||||
if (_stopping) {
|
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
|
||||||
return;
|
dispatch_semaphore_wait(_frameSemaphore, timeout);
|
||||||
}
|
|
||||||
dispatch_semaphore_wait(_frameSemaphore, DISPATCH_TIME_FOREVER);
|
|
||||||
if (_stopping) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +256,12 @@
|
|||||||
CFTimeInterval deadline = start + timeout;
|
CFTimeInterval deadline = start + timeout;
|
||||||
int round = 0;
|
int round = 0;
|
||||||
|
|
||||||
// Always attempt to dequeue at least once
|
if (self.isStopping) {
|
||||||
do {
|
|
||||||
if (_stopping) {
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always attempt to dequeue at least once
|
||||||
|
do {
|
||||||
if (round > 0) {
|
if (round > 0) {
|
||||||
usleep(100); // 0.1ms
|
usleep(100); // 0.1ms
|
||||||
}
|
}
|
||||||
@@ -304,7 +291,6 @@
|
|||||||
os_unfair_lock_lock(&_lock);
|
os_unfair_lock_lock(&_lock);
|
||||||
_head = _tail = _count = 0;
|
_head = _tail = _count = 0;
|
||||||
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
|
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
|
||||||
_stopping = NO;
|
|
||||||
os_unfair_lock_unlock(&_lock);
|
os_unfair_lock_unlock(&_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +324,13 @@
|
|||||||
return cap;
|
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);
|
// For use with NSLog("%@", franeQueue);
|
||||||
- (NSString *)description {
|
- (NSString *)description {
|
||||||
__block NSMutableArray *parts = [NSMutableArray arrayWithCapacity:_count];
|
__block NSMutableArray *parts = [NSMutableArray arrayWithCapacity:_count];
|
||||||
@@ -347,29 +340,4 @@
|
|||||||
return [NSString stringWithFormat:@"[%@]", [parts componentsJoinedByString:@",\n"]];
|
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
|
@end
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
#if defined(NDEBUG)
|
#if defined(NDEBUG)
|
||||||
static LogLevel LoggerLogLevel = LOG_I;
|
static LogLevel LoggerLogLevel = LOG_I;
|
||||||
#else
|
#else
|
||||||
static LogLevel LoggerLogLevel = LOG_D;
|
// Debug is too spammy during discovery
|
||||||
|
// static LogLevel LoggerLogLevel = LOG_D;
|
||||||
|
static LogLevel LoggerLogLevel = LOG_I;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
|
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ typedef struct {
|
|||||||
PlotMetrics decodeMetrics;
|
PlotMetrics decodeMetrics;
|
||||||
PlotMetrics frameQueueMetrics;
|
PlotMetrics frameQueueMetrics;
|
||||||
PlotMetrics frameDropMetrics;
|
PlotMetrics frameDropMetrics;
|
||||||
RenderingBackend renderingBackend;
|
NSString *renderingBackendString;
|
||||||
} video_stats_t;
|
} video_stats_t;
|
||||||
@@ -745,7 +745,6 @@
|
|||||||
_inactivityTimer = nil;
|
_inactivityTimer = nil;
|
||||||
}
|
}
|
||||||
if (self.metalViewController) {
|
if (self.metalViewController) {
|
||||||
[self.metalViewController stop];
|
|
||||||
[self.metalViewController.view removeFromSuperview];
|
[self.metalViewController.view removeFromSuperview];
|
||||||
self.metalViewController = nil;
|
self.metalViewController = nil;
|
||||||
NSLog(@"Metal renderer stopped and cleaned up.");
|
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
|
// This will fire if the user opens control center or gets a low battery message
|
||||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||||
if (self.metalViewController) {
|
|
||||||
[self.metalViewController pause];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//[self.pipController startPictureInPicture];
|
//[self.pipController startPictureInPicture];
|
||||||
//sleep(1);
|
//sleep(1);
|
||||||
|
|
||||||
@@ -947,9 +941,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||||
if (self.metalViewController) {
|
|
||||||
[self.metalViewController resume];
|
|
||||||
}
|
|
||||||
// Stop the background timer, since we're foregrounded again
|
// Stop the background timer, since we're foregrounded again
|
||||||
if (_inactivityTimer != nil) {
|
if (_inactivityTimer != nil) {
|
||||||
Log(LOG_I, @"Stopping inactivity timer after becoming active again");
|
Log(LOG_I, @"Stopping inactivity timer after becoming active again");
|
||||||
|
|||||||
Reference in new issue
Block a user