Source-SCCamera/ManagedCapturer/SCManagedCapturePreviewLaye...

564 lines
23 KiB
Objective-C

//
// SCManagedCapturePreviewLayerController.m
// Snapchat
//
// Created by Liu Liu on 5/5/15.
// Copyright (c) 2015 Snapchat, Inc. All rights reserved.
//
#import "SCManagedCapturePreviewLayerController.h"
#import "SCBlackCameraDetector.h"
#import "SCCameraTweaks.h"
#import "SCManagedCapturePreviewView.h"
#import "SCManagedCapturer.h"
#import "SCManagedCapturerListener.h"
#import "SCManagedCapturerUtils.h"
#import "SCMetalUtils.h"
#import <SCFoundation/NSData+Random.h>
#import <SCFoundation/SCCoreGraphicsUtils.h>
#import <SCFoundation/SCDeviceName.h>
#import <SCFoundation/SCLog.h>
#import <SCFoundation/SCQueuePerformer.h>
#import <SCFoundation/SCTrace.h>
#import <SCFoundation/SCTraceODPCompatible.h>
#import <SCFoundation/UIScreen+SCSafeAreaInsets.h>
#import <SCGhostToSnappable/SCGhostToSnappableSignal.h>
#import <FBKVOController/FBKVOController.h>
#define SCLogPreviewLayerInfo(fmt, ...) SCLogCoreCameraInfo(@"[PreviewLayerController] " fmt, ##__VA_ARGS__)
#define SCLogPreviewLayerWarning(fmt, ...) SCLogCoreCameraWarning(@"[PreviewLayerController] " fmt, ##__VA_ARGS__)
#define SCLogPreviewLayerError(fmt, ...) SCLogCoreCameraError(@"[PreviewLayerController] " fmt, ##__VA_ARGS__)
const static CGSize kSCManagedCapturePreviewDefaultRenderSize = {
.width = 720, .height = 1280,
};
const static CGSize kSCManagedCapturePreviewRenderSize1080p = {
.width = 1080, .height = 1920,
};
#if !TARGET_IPHONE_SIMULATOR
static NSInteger const kSCMetalCannotAcquireDrawableLimit = 2;
@interface CAMetalLayer (SCSecretFature)
// Call discardContents.
- (void)sc_secretFeature;
@end
@implementation CAMetalLayer (SCSecretFature)
- (void)sc_secretFeature
{
// "discardContents"
char buffer[] = {0x9b, 0x96, 0x8c, 0x9c, 0x9e, 0x8d, 0x9b, 0xbc, 0x90, 0x91, 0x8b, 0x9a, 0x91, 0x8b, 0x8c, 0};
unsigned long len = strlen(buffer);
for (unsigned idx = 0; idx < len; ++idx) {
buffer[idx] = ~buffer[idx];
}
SEL selector = NSSelectorFromString([NSString stringWithUTF8String:buffer]);
if ([self respondsToSelector:selector]) {
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:selector];
[invocation invoke];
}
// For anyone curious, here is the actual implementation for discardContents in 10.3 (With Hopper v4, arm64)
// From glance, this seems pretty safe to call.
// void -[CAMetalLayer(CAMetalLayerPrivate) discardContents](int arg0)
// {
// *(r31 + 0xffffffffffffffe0) = r20;
// *(0xfffffffffffffff0 + r31) = r19;
// r31 = r31 + 0xffffffffffffffe0;
// *(r31 + 0x10) = r29;
// *(0x20 + r31) = r30;
// r29 = r31 + 0x10;
// r19 = *(arg0 + sign_extend_64(*(int32_t *)0x1a6300510));
// if (r19 != 0x0) {
// r0 = loc_1807079dc(*0x1a7811fc8, r19);
// r0 = _CAImageQueueConsumeUnconsumed(*(r19 + 0x10));
// r0 = _CAImageQueueFlush(*(r19 + 0x10));
// r29 = *(r31 + 0x10);
// r30 = *(0x20 + r31);
// r20 = *r31;
// r19 = *(r31 + 0x10);
// r31 = r31 + 0x20;
// r0 = loc_1807079dc(*0x1a7811fc8, zero_extend_64(0x0));
// } else {
// r29 = *(r31 + 0x10);
// r30 = *(0x20 + r31);
// r20 = *r31;
// r19 = *(r31 + 0x10);
// r31 = r31 + 0x20;
// }
// return;
// }
}
@end
#endif
@interface SCManagedCapturePreviewLayerController () <SCManagedCapturerListener>
@property (nonatomic) BOOL renderSuspended;
@end
@implementation SCManagedCapturePreviewLayerController {
SCManagedCapturePreviewView *_view;
CGSize _drawableSize;
SCQueuePerformer *_performer;
FBKVOController *_renderingKVO;
#if !TARGET_IPHONE_SIMULATOR
CAMetalLayer *_metalLayer;
id<MTLCommandQueue> _commandQueue;
id<MTLRenderPipelineState> _renderPipelineState;
CVMetalTextureCacheRef _textureCache;
dispatch_semaphore_t _commandBufferSemaphore;
// If the current view contains an outdated display (or any display)
BOOL _containOutdatedPreview;
// If we called empty outdated display already, but for some reason, hasn't emptied it yet.
BOOL _requireToFlushOutdatedPreview;
NSMutableSet *_tokenSet;
NSUInteger _cannotAcquireDrawable;
#endif
}
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
static SCManagedCapturePreviewLayerController *managedCapturePreviewLayerController;
dispatch_once(&onceToken, ^{
managedCapturePreviewLayerController = [[SCManagedCapturePreviewLayerController alloc] init];
});
return managedCapturePreviewLayerController;
}
- (instancetype)init
{
self = [super init];
if (self) {
#if !TARGET_IPHONE_SIMULATOR
// We only allow one renders at a time (Sorry, no double / triple buffering).
// It has to be created early here, otherwise integrity of other parts of the code is not
// guaranteed.
// TODO: I need to reason more about the initialization sequence.
_commandBufferSemaphore = dispatch_semaphore_create(1);
// Set _renderSuspended to be YES so that we won't render until it is fully setup.
_renderSuspended = YES;
_tokenSet = [NSMutableSet set];
#endif
// If the screen is less than default size, we should fallback.
CGFloat nativeScale = [UIScreen mainScreen].nativeScale;
CGSize screenSize = [UIScreen mainScreen].fixedCoordinateSpace.bounds.size;
CGSize renderSize = [SCDeviceName isIphoneX] ? kSCManagedCapturePreviewRenderSize1080p
: kSCManagedCapturePreviewDefaultRenderSize;
if (screenSize.width * nativeScale < renderSize.width) {
_drawableSize = CGSizeMake(screenSize.width * nativeScale, screenSize.height * nativeScale);
} else {
_drawableSize = SCSizeIntegral(
SCSizeCropToAspectRatio(renderSize, SCSizeGetAspectRatio(SCManagedCapturerAllScreenSize())));
}
_performer = [[SCQueuePerformer alloc] initWithLabel:"SCManagedCapturePreviewLayerController"
qualityOfService:QOS_CLASS_USER_INITIATED
queueType:DISPATCH_QUEUE_SERIAL
context:SCQueuePerformerContextCoreCamera];
_renderingKVO = [[FBKVOController alloc] initWithObserver:self];
[_renderingKVO observe:self
keyPath:@keypath(self, renderSuspended)
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
block:^(id observer, id object, NSDictionary *change) {
BOOL oldValue = [change[NSKeyValueChangeOldKey] boolValue];
BOOL newValue = [change[NSKeyValueChangeNewKey] boolValue];
if (oldValue != newValue) {
[[_delegate blackCameraDetectorForManagedCapturePreviewLayerController:self]
capturePreviewDidBecomeVisible:!newValue];
}
}];
}
return self;
}
- (void)pause
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SCLogPreviewLayerInfo(@"pause Metal rendering performer waiting");
[_performer performAndWait:^() {
self.renderSuspended = YES;
}];
SCLogPreviewLayerInfo(@"pause Metal rendering performer finished");
#endif
}
- (void)resume
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SCLogPreviewLayerInfo(@"resume Metal rendering performer waiting");
[_performer performAndWait:^() {
self.renderSuspended = NO;
}];
SCLogPreviewLayerInfo(@"resume Metal rendering performer finished");
#endif
}
- (void)setupPreviewLayer
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SCAssertMainThread();
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
if (!_metalLayer) {
_metalLayer = [CAMetalLayer new];
SCLogPreviewLayerInfo(@"setup metalLayer:%@", _metalLayer);
if (!_view) {
// Create capture preview view and setup the metal layer
[self view];
} else {
[_view setupMetalLayer:_metalLayer];
}
}
#endif
}
- (UIView *)newStandInViewWithRect:(CGRect)rect
{
return [self.view resizableSnapshotViewFromRect:rect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
}
- (void)setupRenderPipeline
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
SCAssertNotMainThread();
id<MTLDevice> device = SCGetManagedCaptureMetalDevice();
id<MTLLibrary> shaderLibrary = [device newDefaultLibrary];
_commandQueue = [device newCommandQueue];
MTLRenderPipelineDescriptor *renderPipelineDescriptor = [MTLRenderPipelineDescriptor new];
renderPipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
renderPipelineDescriptor.vertexFunction = [shaderLibrary newFunctionWithName:@"yuv_vertex_reshape"];
renderPipelineDescriptor.fragmentFunction = [shaderLibrary newFunctionWithName:@"yuv_fragment_texture"];
MTLVertexDescriptor *vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
vertexDescriptor.attributes[0].offset = 0;
vertexDescriptor.attributes[0].bufferIndex = 0;
vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords
vertexDescriptor.attributes[1].offset = 2 * sizeof(float);
vertexDescriptor.attributes[1].bufferIndex = 0;
vertexDescriptor.layouts[0].stepRate = 1;
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
vertexDescriptor.layouts[0].stride = 4 * sizeof(float);
renderPipelineDescriptor.vertexDescriptor = vertexDescriptor;
_renderPipelineState = [device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:nil];
CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &_textureCache);
_metalLayer.device = device;
_metalLayer.drawableSize = _drawableSize;
_metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
_metalLayer.framebufferOnly = YES; // It is default to Yes.
[_performer performAndWait:^() {
self.renderSuspended = NO;
}];
SCLogPreviewLayerInfo(@"did setup render pipeline");
#endif
}
- (UIView *)view
{
SCTraceStart();
SCAssertMainThread();
if (!_view) {
#if TARGET_IPHONE_SIMULATOR
_view = [[SCManagedCapturePreviewView alloc] initWithFrame:[UIScreen mainScreen].fixedCoordinateSpace.bounds
aspectRatio:SCSizeGetAspectRatio(_drawableSize)
metalLayer:nil];
#else
_view = [[SCManagedCapturePreviewView alloc] initWithFrame:[UIScreen mainScreen].fixedCoordinateSpace.bounds
aspectRatio:SCSizeGetAspectRatio(_drawableSize)
metalLayer:_metalLayer];
SCLogPreviewLayerInfo(@"created SCManagedCapturePreviewView:%@", _view);
#endif
}
return _view;
}
- (void)setManagedCapturer:(id<SCCapturer>)managedCapturer
{
SCTraceStart();
SCLogPreviewLayerInfo(@"setManagedCapturer:%@", managedCapturer);
if (SCDeviceSupportsMetal()) {
[managedCapturer addSampleBufferDisplayController:self context:SCCapturerContext];
}
[managedCapturer addListener:self];
}
- (void)applicationDidEnterBackground
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SCAssertMainThread();
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
SCLogPreviewLayerInfo(@"applicationDidEnterBackground waiting for performer");
[_performer performAndWait:^() {
CVMetalTextureCacheFlush(_textureCache, 0);
[_tokenSet removeAllObjects];
self.renderSuspended = YES;
}];
SCLogPreviewLayerInfo(@"applicationDidEnterBackground signal performer finishes");
#endif
}
- (void)applicationWillResignActive
{
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
SCTraceStart();
SCAssertMainThread();
#if !TARGET_IPHONE_SIMULATOR
SCLogPreviewLayerInfo(@"pause Metal rendering");
[_performer performAndWait:^() {
self.renderSuspended = YES;
}];
#endif
}
- (void)applicationDidBecomeActive
{
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
SCTraceStart();
SCAssertMainThread();
#if !TARGET_IPHONE_SIMULATOR
SCLogPreviewLayerInfo(@"resume Metal rendering waiting for performer");
[_performer performAndWait:^() {
self.renderSuspended = NO;
}];
SCLogPreviewLayerInfo(@"resume Metal rendering performer finished");
#endif
}
- (void)applicationWillEnterForeground
{
#if !TARGET_IPHONE_SIMULATOR
SCTraceStart();
SCAssertMainThread();
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
SCLogPreviewLayerInfo(@"applicationWillEnterForeground waiting for performer");
[_performer performAndWait:^() {
self.renderSuspended = NO;
if (_containOutdatedPreview && _tokenSet.count == 0) {
[self _flushOutdatedPreview];
}
}];
SCLogPreviewLayerInfo(@"applicationWillEnterForeground performer finished");
#endif
}
- (NSString *)keepDisplayingOutdatedPreview
{
SCTraceStart();
NSString *token = [NSData randomBase64EncodedStringOfLength:8];
#if !TARGET_IPHONE_SIMULATOR
SCLogPreviewLayerInfo(@"keepDisplayingOutdatedPreview waiting for performer");
[_performer performAndWait:^() {
[_tokenSet addObject:token];
}];
SCLogPreviewLayerInfo(@"keepDisplayingOutdatedPreview performer finished");
#endif
return token;
}
- (void)endDisplayingOutdatedPreview:(NSString *)keepToken
{
#if !TARGET_IPHONE_SIMULATOR
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal());
// I simply use a lock for this. If it becomes a bottleneck, I can figure something else out.
SCTraceStart();
SCLogPreviewLayerInfo(@"endDisplayingOutdatedPreview waiting for performer");
[_performer performAndWait:^() {
[_tokenSet removeObject:keepToken];
if (_tokenSet.count == 0 && _requireToFlushOutdatedPreview && _containOutdatedPreview && !_renderSuspended) {
[self _flushOutdatedPreview];
}
}];
SCLogPreviewLayerInfo(@"endDisplayingOutdatedPreview performer finished");
#endif
}
#pragma mark - SCManagedSampleBufferDisplayController
- (void)enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
#if !TARGET_IPHONE_SIMULATOR
// Just drop the frame if it is rendering.
SC_GUARD_ELSE_RUN_AND_RETURN_VALUE(dispatch_semaphore_wait(_commandBufferSemaphore, DISPATCH_TIME_NOW) == 0,
SCLogPreviewLayerInfo(@"waiting for commandBufferSemaphore signaled"), );
// Just drop the frame, simple.
[_performer performAndWait:^() {
if (_renderSuspended) {
SCLogGeneralInfo(@"Preview rendering suspends and current sample buffer is dropped");
dispatch_semaphore_signal(_commandBufferSemaphore);
return;
}
@autoreleasepool {
const BOOL isFirstPreviewFrame = !_containOutdatedPreview;
if (isFirstPreviewFrame) {
// Signal that we receieved the first frame (otherwise this will be YES already).
SCGhostToSnappableSignalDidReceiveFirstPreviewFrame();
sc_create_g2s_ticket_f func = [_delegate g2sTicketForManagedCapturePreviewLayerController:self];
SCG2SActivateManiphestTicketQueueWithTicketCreationFunction(func);
}
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
size_t pixelWidth = CVPixelBufferGetWidth(imageBuffer);
size_t pixelHeight = CVPixelBufferGetHeight(imageBuffer);
id<MTLTexture> yTexture =
SCMetalTextureFromPixelBuffer(imageBuffer, 0, MTLPixelFormatR8Unorm, _textureCache);
id<MTLTexture> cbCrTexture =
SCMetalTextureFromPixelBuffer(imageBuffer, 1, MTLPixelFormatRG8Unorm, _textureCache);
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
SC_GUARD_ELSE_RUN_AND_RETURN(yTexture && cbCrTexture, dispatch_semaphore_signal(_commandBufferSemaphore));
id<MTLCommandBuffer> commandBuffer = _commandQueue.commandBuffer;
id<CAMetalDrawable> drawable = _metalLayer.nextDrawable;
if (!drawable) {
// Count how many times I cannot acquire drawable.
++_cannotAcquireDrawable;
if (_cannotAcquireDrawable >= kSCMetalCannotAcquireDrawableLimit) {
// Calling [_metalLayer discardContents] to flush the CAImageQueue
SCLogGeneralInfo(@"Cannot acquire drawable, reboot Metal ..");
[_metalLayer sc_secretFeature];
}
dispatch_semaphore_signal(_commandBufferSemaphore);
return;
}
_cannotAcquireDrawable = 0; // Reset to 0 in case we can acquire drawable.
MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
id<MTLRenderCommandEncoder> renderEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder setRenderPipelineState:_renderPipelineState];
[renderEncoder setFragmentTexture:yTexture atIndex:0];
[renderEncoder setFragmentTexture:cbCrTexture atIndex:1];
// TODO: Prob this out of the image buffer.
// 90 clock-wise rotated texture coordinate.
// Also do aspect fill.
float normalizedHeight, normalizedWidth;
if (pixelWidth * _drawableSize.width > _drawableSize.height * pixelHeight) {
normalizedHeight = 1.0;
normalizedWidth = pixelWidth * (_drawableSize.width / pixelHeight) / _drawableSize.height;
} else {
normalizedHeight = pixelHeight * (_drawableSize.height / pixelWidth) / _drawableSize.width;
normalizedWidth = 1.0;
}
const float vertices[] = {
-normalizedHeight, -normalizedWidth, 1, 1, // lower left -> upper right
normalizedHeight, -normalizedWidth, 1, 0, // lower right -> lower right
-normalizedHeight, normalizedWidth, 0, 1, // upper left -> upper left
normalizedHeight, normalizedWidth, 0, 0, // upper right -> lower left
};
[renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
[renderEncoder endEncoding];
// I need to set a minimum duration for the drawable.
// There is a bug on iOS 10.3, if I present as soon as I can, I am keeping the GPU
// at 30fps even you swipe between views, that causes undesirable visual jarring.
// By set a minimum duration, even it is incrediably small (I tried 10ms, and here 60fps works),
// the OS seems can adjust the frame rate much better when swiping.
// This is an iOS 10.3 new method.
if ([commandBuffer respondsToSelector:@selector(presentDrawable:afterMinimumDuration:)]) {
[(id)commandBuffer presentDrawable:drawable afterMinimumDuration:(1.0 / 60)];
} else {
[commandBuffer presentDrawable:drawable];
}
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
dispatch_semaphore_signal(_commandBufferSemaphore);
}];
if (isFirstPreviewFrame) {
if ([drawable respondsToSelector:@selector(addPresentedHandler:)] &&
[drawable respondsToSelector:@selector(presentedTime)]) {
[(id)drawable addPresentedHandler:^(id<MTLDrawable> presentedDrawable) {
SCGhostToSnappableSignalDidRenderFirstPreviewFrame([(id)presentedDrawable presentedTime]);
}];
} else {
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
// Using CACurrentMediaTime to approximate.
SCGhostToSnappableSignalDidRenderFirstPreviewFrame(CACurrentMediaTime());
}];
}
}
// We enqueued an sample buffer to display, therefore, it contains an outdated display (to be clean up).
_containOutdatedPreview = YES;
[commandBuffer commit];
}
}];
#endif
}
- (void)flushOutdatedPreview
{
SCTraceStart();
#if !TARGET_IPHONE_SIMULATOR
// This method cannot drop frames (otherwise we will have residual on the screen).
SCLogPreviewLayerInfo(@"flushOutdatedPreview waiting for performer");
[_performer performAndWait:^() {
_requireToFlushOutdatedPreview = YES;
SC_GUARD_ELSE_RETURN(!_renderSuspended);
// Have to make sure we have no token left before return.
SC_GUARD_ELSE_RETURN(_tokenSet.count == 0);
[self _flushOutdatedPreview];
}];
SCLogPreviewLayerInfo(@"flushOutdatedPreview performer finished");
#endif
}
- (void)_flushOutdatedPreview
{
SCTraceStart();
SCAssertPerformer(_performer);
#if !TARGET_IPHONE_SIMULATOR
SCLogPreviewLayerInfo(@"flushOutdatedPreview containOutdatedPreview:%d", _containOutdatedPreview);
// I don't care if this has renderSuspended or not, assuming I did the right thing.
// Emptied, no need to do this any more on foregrounding.
SC_GUARD_ELSE_RETURN(_containOutdatedPreview);
_containOutdatedPreview = NO;
_requireToFlushOutdatedPreview = NO;
[_metalLayer sc_secretFeature];
#endif
}
#pragma mark - SCManagedCapturerListener
- (void)managedCapturer:(id<SCCapturer>)managedCapturer
didChangeVideoPreviewLayer:(AVCaptureVideoPreviewLayer *)videoPreviewLayer
{
SCTraceStart();
SCAssertMainThread();
// Force to load the view
[self view];
_view.videoPreviewLayer = videoPreviewLayer;
SCLogPreviewLayerInfo(@"didChangeVideoPreviewLayer:%@", videoPreviewLayer);
}
- (void)managedCapturer:(id<SCCapturer>)managedCapturer didChangeVideoPreviewGLView:(LSAGLView *)videoPreviewGLView
{
SCTraceStart();
SCAssertMainThread();
// Force to load the view
[self view];
_view.videoPreviewGLView = videoPreviewGLView;
SCLogPreviewLayerInfo(@"didChangeVideoPreviewGLView:%@", videoPreviewGLView);
}
@end