From 30f5a97ea188e440fb10e1541e60cb36bdfcaddc Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 3 Nov 2020 12:00:03 -0700 Subject: [PATCH] Consolidate surface-related functionality into MetalSwapChain (#3246) --- filament/backend/src/metal/MetalContext.h | 23 +--- filament/backend/src/metal/MetalContext.mm | 88 ------------ filament/backend/src/metal/MetalDriver.mm | 19 +-- filament/backend/src/metal/MetalHandles.h | 62 ++++++--- filament/backend/src/metal/MetalHandles.mm | 150 ++++++++++++++++++++- 5 files changed, 197 insertions(+), 145 deletions(-) diff --git a/filament/backend/src/metal/MetalContext.h b/filament/backend/src/metal/MetalContext.h index dba9b5a6f0..9a08aa233d 100644 --- a/filament/backend/src/metal/MetalContext.h +++ b/filament/backend/src/metal/MetalContext.h @@ -45,15 +45,6 @@ struct MetalContext { id pendingCommandBuffer = nullptr; id currentRenderPassEncoder = nullptr; - // These two fields store a callback and user data to notify the client that a frame is ready - // for presentation. - // If frameFinishedCallback is nullptr, then the Metal backend automatically calls - // presentDrawable when the frame is commited. - // Otherwise, the Metal backend will not automatically present the frame. Instead, clients bear - // the responsibility of presenting the frame by calling the PresentCallable object. - backend::FrameFinishedCallback frameFinishedCallback = nullptr; - void* frameFinishedUserData = nullptr; - // Tracks resources used by command buffers. MetalResourceTracker resourceTracker; @@ -75,11 +66,7 @@ struct MetalContext { MetalBufferPool* bufferPool; - // Surface-related properties. - MetalSwapChain* currentSurface = nullptr; - id currentDrawable = nil; - id currentDepthTexture = nil; - id headlessDrawable = nil; + MetalSwapChain* currentSurface = nil; // External textures. CVMetalTextureCacheRef textureCache = nullptr; @@ -98,14 +85,6 @@ struct MetalContext { TimerQueryInterface* timerQueryImpl; }; -// Acquire the current surface's CAMetalDrawable for the current frame if it has not already been -// acquired. This method stores it in the context's currentDrawable field and returns the -// drawable's texture. -// For headless swapchains a new texture is created. -id acquireDrawable(MetalContext* context); - -id acquireDepthTexture(MetalContext* context); - id getPendingCommandBuffer(MetalContext* context); void submitPendingCommands(MetalContext* context); diff --git a/filament/backend/src/metal/MetalContext.mm b/filament/backend/src/metal/MetalContext.mm index d0f36adebf..6db5f1e03f 100644 --- a/filament/backend/src/metal/MetalContext.mm +++ b/filament/backend/src/metal/MetalContext.mm @@ -24,94 +24,6 @@ namespace filament { namespace backend { namespace metal { -void presentDrawable(bool presentFrame, void* user) { - // CFBridgingRelease here is used to balance the CFBridgingRetain inside of acquireDrawable. - id drawable = (id) CFBridgingRelease(user); - if (presentFrame) { - [drawable present]; - } - // The drawable will be released here when the "drawable" variable goes out of scope. -} - -id acquireDrawable(MetalContext* context) { - if (context->currentDrawable) { - return context->currentDrawable.texture; - } - if (context->currentSurface->isHeadless()) { - if (context->headlessDrawable) { - return context->headlessDrawable; - } - // For headless surfaces we construct a "fake" drawable, which is simply a renderable - // texture. - MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor new]; - textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; - textureDescriptor.width = context->currentSurface->getSurfaceWidth(); - textureDescriptor.height = context->currentSurface->getSurfaceHeight(); - // Specify MTLTextureUsageShaderRead so the headless surface can be blitted from. - textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; -#if defined(IOS) - textureDescriptor.storageMode = MTLStorageModeShared; -#else - textureDescriptor.storageMode = MTLStorageModeManaged; -#endif - context->headlessDrawable = [context->device newTextureWithDescriptor:textureDescriptor]; - return context->headlessDrawable; - } - - context->currentDrawable = [context->currentSurface->getLayer() nextDrawable]; - - if (context->frameFinishedCallback) { - id drawable = context->currentDrawable; - backend::FrameFinishedCallback callback = context->frameFinishedCallback; - void* userData = context->frameFinishedUserData; - // This block strongly captures drawable to keep it alive until the handler executes. - [getPendingCommandBuffer(context) addScheduledHandler:^(id cb) { - // CFBridgingRetain is used here to give the drawable a +1 retain count before - // casting it to a void*. - PresentCallable callable(presentDrawable, (void*) CFBridgingRetain(drawable)); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - callback(callable, userData); - }); - }]; - } - ASSERT_POSTCONDITION(context->currentDrawable != nil, "Could not obtain drawable."); - return context->currentDrawable.texture; -} - -id acquireDepthTexture(MetalContext* context) { - if (context->currentDepthTexture) { - // If the surface size has changed, we'll need to allocate a new depth texture. - if (context->currentDepthTexture.width != context->currentSurface->getSurfaceWidth() || - context->currentDepthTexture.height != context->currentSurface->getSurfaceHeight()) { - context->currentDepthTexture = nil; - } else { - return context->currentDepthTexture; - } - } - - const MTLPixelFormat depthFormat = -#if defined(IOS) - MTLPixelFormatDepth32Float; -#else - context->device.depth24Stencil8PixelFormatSupported ? - MTLPixelFormatDepth24Unorm_Stencil8 : MTLPixelFormatDepth32Float; -#endif - - const NSUInteger width = context->currentSurface->getSurfaceWidth(); - const NSUInteger height = context->currentSurface->getSurfaceHeight(); - MTLTextureDescriptor* descriptor = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:depthFormat - width:width - height:height - mipmapped:NO]; - descriptor.usage = MTLTextureUsageRenderTarget; - descriptor.resourceOptions = MTLResourceStorageModePrivate; - - context->currentDepthTexture = [context->device newTextureWithDescriptor:descriptor]; - - return context->currentDepthTexture; -} - id getPendingCommandBuffer(MetalContext* context) { if (context->pendingCommandBuffer) { return context->pendingCommandBuffer; diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index c94f79a0ce..327a74652f 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -103,9 +103,7 @@ void MetalDriver::tick(int) { void MetalDriver::beginFrame(int64_t monotonic_clock_ns, uint32_t frameId, backend::FrameFinishedCallback callback, void* user) { - // If a callback was specified, then the client is responsible for presenting the frame. - mContext->frameFinishedCallback = callback; - mContext->frameFinishedUserData = user; + mContext->currentSurface->setFrameFinishedCallback(callback, user); } void MetalDriver::execute(std::function fn) noexcept { @@ -125,8 +123,7 @@ void MetalDriver::endFrame(uint32_t frameId) { mContext->bufferPool->gc(); // If we acquired a drawable for this frame, ensure that we release it here. - mContext->currentDrawable = nil; - mContext->headlessDrawable = nil; + mContext->currentSurface->releaseDrawable(); CVMetalTextureCacheFlush(mContext->textureCache, 0); } @@ -281,12 +278,12 @@ void MetalDriver::createSyncR(Handle sh, int) { void MetalDriver::createSwapChainR(Handle sch, void* nativeWindow, uint64_t flags) { auto* metalLayer = (__bridge CAMetalLayer*) nativeWindow; - construct_handle(mHandleMap, sch, mContext->device, metalLayer, flags); + construct_handle(mHandleMap, sch, *mContext, metalLayer, flags); } void MetalDriver::createSwapChainHeadlessR(Handle sch, uint32_t width, uint32_t height, uint64_t flags) { - construct_handle(mHandleMap, sch, width, height, flags); + construct_handle(mHandleMap, sch, *mContext, width, height, flags); } void MetalDriver::createStreamFromTextureIdR(Handle, intptr_t externalTextureId, @@ -800,12 +797,10 @@ void MetalDriver::makeCurrent(Handle schDraw, Handle s } void MetalDriver::commit(Handle sch) { - if (mContext->currentDrawable != nil && !mContext->frameFinishedCallback) { - [getPendingCommandBuffer(mContext) presentDrawable:mContext->currentDrawable]; - } + auto* swapChain = handle_cast(mHandleMap, sch); + swapChain->present(); submitPendingCommands(mContext); - mContext->currentDrawable = nil; - mContext->headlessDrawable = nil; + swapChain->releaseDrawable(); } void MetalDriver::bindUniformBuffer(size_t index, Handle ubh) { diff --git a/filament/backend/src/metal/MetalHandles.h b/filament/backend/src/metal/MetalHandles.h index 4c096dc60c..b3a24ef050 100644 --- a/filament/backend/src/metal/MetalHandles.h +++ b/filament/backend/src/metal/MetalHandles.h @@ -44,33 +44,63 @@ namespace metal { class MetalSwapChain : public HwSwapChain { public: - MetalSwapChain(id device, CAMetalLayer* nativeWindow, uint64_t flags); + // Instantiate a SwapChain from a CAMetalLayer + MetalSwapChain(MetalContext& context, CAMetalLayer* nativeWindow, uint64_t flags); // Instantiate a headless SwapChain. - MetalSwapChain(int32_t width, int32_t height, uint64_t flags); + MetalSwapChain(MetalContext& context, int32_t width, int32_t height, uint64_t flags); - bool isHeadless() const { return layer == nullptr; } - CAMetalLayer* getLayer() const { return layer; } + ~MetalSwapChain(); - NSUInteger getSurfaceWidth() const { - if (isHeadless()) { - return headlessWidth; - } - return (NSUInteger) layer.drawableSize.width; - } + // Acquires a texture that can be used to render into this SwapChain. + // The texture source depends on the type of SwapChain: + // - CAMetalLayer-backed: acquires the CAMetalDrawable and returns its texture. + // - Headless: lazily creates and returns a headless texture. + id acquireDrawable(); - NSUInteger getSurfaceHeight() const { - if (isHeadless()) { - return headlessHeight; - } - return (NSUInteger) layer.drawableSize.height; - } + id acquireDepthTexture(); + + void releaseDrawable(); + + void setFrameFinishedCallback(FrameFinishedCallback callback, void* user); + + // For CAMetalLayer-backed SwapChains, presents the drawable or schedules a + // FrameFinishedCallback. + void present(); + + NSUInteger getSurfaceWidth() const; + NSUInteger getSurfaceHeight() const; private: + enum class SwapChainType { + CAMETALLAYER, + CVPIXELBUFFERREF, + HEADLESS + }; + bool isCaMetalLayer() const { return type == SwapChainType::CAMETALLAYER; } + bool isHeadless() const { return type == SwapChainType::HEADLESS; } + bool isPixelBuffer() const { return type == SwapChainType::CVPIXELBUFFERREF; } + + void scheduleFrameFinishedCallback(); + + MetalContext& context; + id drawable = nil; + id depthTexture = nil; + id headlessDrawable = nil; NSUInteger headlessWidth; NSUInteger headlessHeight; CAMetalLayer* layer = nullptr; + SwapChainType type; + + // These two fields store a callback and user data to notify the client that a frame is ready + // for presentation. + // If frameFinishedCallback is nullptr, then the Metal backend automatically calls + // presentDrawable when the frame is commited. + // Otherwise, the Metal backend will not automatically present the frame. Instead, clients bear + // the responsibility of presenting the frame by calling the PresentCallable object. + FrameFinishedCallback frameFinishedCallback = nullptr; + void* frameFinishedUserData = nullptr; }; struct MetalVertexBuffer : public HwVertexBuffer { diff --git a/filament/backend/src/metal/MetalHandles.mm b/filament/backend/src/metal/MetalHandles.mm index 9de34a5b24..a3b0a8ad26 100644 --- a/filament/backend/src/metal/MetalHandles.mm +++ b/filament/backend/src/metal/MetalHandles.mm @@ -49,8 +49,8 @@ static inline MTLTextureUsage getMetalTextureUsage(TextureUsage usage) { return MTLTextureUsage(u); } -MetalSwapChain::MetalSwapChain(id device, CAMetalLayer* nativeWindow, uint64_t flags) - : layer(nativeWindow) { +MetalSwapChain::MetalSwapChain(MetalContext& context, CAMetalLayer* nativeWindow, uint64_t flags) + : context(context), layer(nativeWindow), type(SwapChainType::CAMETALLAYER) { if (!(flags & SwapChain::CONFIG_TRANSPARENT) && !nativeWindow.opaque) { utils::slog.w << "Warning: Filament SwapChain has no CONFIG_TRANSPARENT flag, " @@ -68,11 +68,147 @@ MetalSwapChain::MetalSwapChain(id device, CAMetalLayer* nativeWindow, nativeWindow.framebufferOnly = NO; } - layer.device = device; + layer.device = context.device; } -MetalSwapChain::MetalSwapChain(int32_t width, int32_t height, uint64_t flags) : headlessWidth(width), - headlessHeight(height) { } +MetalSwapChain::MetalSwapChain(MetalContext& context, int32_t width, int32_t height, uint64_t flags) + : context(context), headlessWidth(width), headlessHeight(height), + type(SwapChainType::HEADLESS) { } + + +NSUInteger MetalSwapChain::getSurfaceWidth() const { + if (isHeadless()) { + return headlessWidth; + } + return (NSUInteger) layer.drawableSize.width; +} + +NSUInteger MetalSwapChain::getSurfaceHeight() const { + if (isHeadless()) { + return headlessHeight; + } + return (NSUInteger) layer.drawableSize.height; +} + +id MetalSwapChain::acquireDrawable() { + if (drawable) { + return drawable.texture; + } + + if (isHeadless()) { + if (headlessDrawable) { + return headlessDrawable; + } + // For headless surfaces we construct a "fake" drawable, which is simply a renderable + // texture. + MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor new]; + textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; + textureDescriptor.width = headlessWidth; + textureDescriptor.height = headlessHeight; + // Specify MTLTextureUsageShaderRead so the headless surface can be blitted from. + textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; +#if defined(IOS) + textureDescriptor.storageMode = MTLStorageModeShared; +#else + textureDescriptor.storageMode = MTLStorageModeManaged; +#endif + headlessDrawable = [context.device newTextureWithDescriptor:textureDescriptor]; + return headlessDrawable; + } + + assert(isCaMetalLayer()); + drawable = [layer nextDrawable]; + + ASSERT_POSTCONDITION(drawable != nil, "Could not obtain drawable."); + return drawable.texture; +} + +void MetalSwapChain::releaseDrawable() { + drawable = nil; +} + +id MetalSwapChain::acquireDepthTexture() { + if (depthTexture) { + // If the surface size has changed, we'll need to allocate a new depth texture. + if (depthTexture.width != getSurfaceWidth() || + depthTexture.height != getSurfaceHeight()) { + depthTexture = nil; + } else { + return depthTexture; + } + } + + const MTLPixelFormat depthFormat = +#if defined(IOS) + MTLPixelFormatDepth32Float; +#else + context.device.depth24Stencil8PixelFormatSupported ? + MTLPixelFormatDepth24Unorm_Stencil8 : MTLPixelFormatDepth32Float; +#endif + + const NSUInteger width = getSurfaceWidth(); + const NSUInteger height = getSurfaceHeight(); + MTLTextureDescriptor* descriptor = + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:depthFormat + width:width + height:height + mipmapped:NO]; + descriptor.usage = MTLTextureUsageRenderTarget; + descriptor.resourceOptions = MTLResourceStorageModePrivate; + + depthTexture = [context.device newTextureWithDescriptor:descriptor]; + + return depthTexture; +} + +void MetalSwapChain::setFrameFinishedCallback(FrameFinishedCallback callback, void* user) { + frameFinishedCallback = callback; + frameFinishedUserData = user; +} + +void MetalSwapChain::present() { + if (drawable) { + if (frameFinishedCallback) { + scheduleFrameFinishedCallback(); + } else { + [getPendingCommandBuffer(&context) presentDrawable:drawable]; + } + } +} + +MetalSwapChain::~MetalSwapChain() { +} + +void presentDrawable(bool presentFrame, void* user) { + // CFBridgingRelease here is used to balance the CFBridgingRetain inside of acquireDrawable. + id drawable = (id) CFBridgingRelease(user); + if (presentFrame) { + [drawable present]; + } + // The drawable will be released here when the "drawable" variable goes out of scope. +} + +void MetalSwapChain::scheduleFrameFinishedCallback() { + if (!frameFinishedCallback) { + return; + } + + assert(drawable); + backend::FrameFinishedCallback callback = frameFinishedCallback; + // This block strongly captures drawable to keep it alive until the handler executes. + // We cannot simply reference this->drawable inside the block because the block would then only + // capture the _this_ pointer (MetalSwapChain*) instead of the drawable. + id d = drawable; + void* userData = frameFinishedUserData; + [getPendingCommandBuffer(&context) addScheduledHandler:^(id cb) { + // CFBridgingRetain is used here to give the drawable a +1 retain count before + // casting it to a void*. + PresentCallable callable(presentDrawable, (void*) CFBridgingRetain(d)); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + callback(callable, userData); + }); + }]; +} MetalVertexBuffer::MetalVertexBuffer(MetalContext& context, uint8_t bufferCount, uint8_t attributeCount, uint32_t vertexCount, AttributeArray const& attributes) @@ -604,7 +740,7 @@ MetalRenderTarget::Attachment MetalRenderTarget::getColorAttachment(size_t index assert(index < MRT::TARGET_COUNT); Attachment result = color[index]; if (index == 0 && defaultRenderTarget) { - result.texture = acquireDrawable(context); + result.texture = context->currentSurface->acquireDrawable(); } return result; } @@ -612,7 +748,7 @@ MetalRenderTarget::Attachment MetalRenderTarget::getColorAttachment(size_t index MetalRenderTarget::Attachment MetalRenderTarget::getDepthAttachment() { Attachment result = depth; if (defaultRenderTarget) { - result.texture = acquireDepthTexture(context); + result.texture = context->currentSurface->acquireDepthTexture(); } return result; }