| // |
| // Copyright 2019 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // SurfaceMtl.mm: |
| // Implements the class methods for SurfaceMtl. |
| // |
| |
| #include "libANGLE/renderer/metal/SurfaceMtl.h" |
| |
| #include <TargetConditionals.h> |
| |
| #include "libANGLE/Display.h" |
| #include "libANGLE/Surface.h" |
| #include "libANGLE/renderer/metal/ContextMtl.h" |
| #include "libANGLE/renderer/metal/DisplayMtl.h" |
| #include "libANGLE/renderer/metal/FrameBufferMtl.h" |
| #include "libANGLE/renderer/metal/mtl_format_utils.h" |
| |
| // Compiler can turn on programmatical frame capture in release build by defining |
| // ANGLE_METAL_FRAME_CAPTURE flag. |
| #if defined(NDEBUG) && !defined(ANGLE_METAL_FRAME_CAPTURE) |
| # define ANGLE_METAL_FRAME_CAPTURE_ENABLED 0 |
| #else |
| # define ANGLE_METAL_FRAME_CAPTURE_ENABLED 1 |
| #endif |
| |
| namespace rx |
| { |
| |
| namespace |
| { |
| constexpr angle::FormatID kDefaultFrameBufferDepthFormatId = angle::FormatID::D32_FLOAT; |
| constexpr angle::FormatID kDefaultFrameBufferStencilFormatId = angle::FormatID::S8_UINT; |
| constexpr angle::FormatID kDefaultFrameBufferDepthStencilFormatId = |
| angle::FormatID::D24_UNORM_S8_UINT; |
| |
| ANGLE_MTL_UNUSED |
| bool IsFrameCaptureEnabled() |
| { |
| #if !ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| return false; |
| #else |
| // We only support frame capture programmatically if the ANGLE_METAL_FRAME_CAPTURE |
| // environment flag is set. Otherwise, it will slow down the rendering. This allows user to |
| // finely control whether he wants to capture the frame for particular application or not. |
| auto var = std::getenv("ANGLE_METAL_FRAME_CAPTURE"); |
| static const bool enabled = var ? (strcmp(var, "1") == 0) : false; |
| |
| return enabled; |
| #endif |
| } |
| |
| ANGLE_MTL_UNUSED |
| size_t MaxAllowedFrameCapture() |
| { |
| #if !ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| return 0; |
| #else |
| auto var = std::getenv("ANGLE_METAL_FRAME_CAPTURE_MAX"); |
| static const size_t maxFrames = var ? std::atoi(var) : 100; |
| |
| return maxFrames; |
| #endif |
| } |
| |
| ANGLE_MTL_UNUSED |
| size_t MinAllowedFrameCapture() |
| { |
| #if !ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| return 0; |
| #else |
| auto var = std::getenv("ANGLE_METAL_FRAME_CAPTURE_MIN"); |
| static const size_t minFrame = var ? std::atoi(var) : 0; |
| |
| return minFrame; |
| #endif |
| } |
| |
| ANGLE_MTL_UNUSED |
| bool FrameCaptureDeviceScope() |
| { |
| #if !ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| return false; |
| #else |
| auto var = std::getenv("ANGLE_METAL_FRAME_CAPTURE_SCOPE"); |
| static const bool scopeDevice = var ? (strcmp(var, "device") == 0) : false; |
| |
| return scopeDevice; |
| #endif |
| } |
| |
| ANGLE_MTL_UNUSED |
| std::atomic<size_t> gFrameCaptured(0); |
| |
| ANGLE_MTL_UNUSED |
| void StartFrameCapture(id<MTLDevice> metalDevice, id<MTLCommandQueue> metalCmdQueue) |
| { |
| #if ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| if (!IsFrameCaptureEnabled()) |
| { |
| return; |
| } |
| |
| if (gFrameCaptured >= MaxAllowedFrameCapture()) |
| { |
| return; |
| } |
| |
| MTLCaptureManager *captureManager = [MTLCaptureManager sharedCaptureManager]; |
| if (captureManager.isCapturing) |
| { |
| return; |
| } |
| |
| gFrameCaptured++; |
| |
| if (gFrameCaptured < MinAllowedFrameCapture()) |
| { |
| return; |
| } |
| |
| # ifdef __MAC_10_15 |
| if (ANGLE_APPLE_AVAILABLE_XCI(10.15, 13.0, 13)) |
| { |
| MTLCaptureDescriptor *captureDescriptor = [[MTLCaptureDescriptor alloc] init]; |
| captureDescriptor.captureObject = metalDevice; |
| |
| NSError *error; |
| if (![captureManager startCaptureWithDescriptor:captureDescriptor error:&error]) |
| { |
| NSLog(@"Failed to start capture, error %@", error); |
| } |
| } |
| else |
| # endif // __MAC_10_15 |
| { |
| if (FrameCaptureDeviceScope()) |
| { |
| [captureManager startCaptureWithDevice:metalDevice]; |
| } |
| else |
| { |
| [captureManager startCaptureWithCommandQueue:metalCmdQueue]; |
| } |
| } |
| #endif // ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| } |
| |
| void StartFrameCapture(ContextMtl *context) |
| { |
| StartFrameCapture(context->getMetalDevice(), context->cmdQueue().get()); |
| } |
| |
| void StopFrameCapture() |
| { |
| #if ANGLE_METAL_FRAME_CAPTURE_ENABLED |
| if (!IsFrameCaptureEnabled()) |
| { |
| return; |
| } |
| MTLCaptureManager *captureManager = [MTLCaptureManager sharedCaptureManager]; |
| if (captureManager.isCapturing) |
| { |
| [captureManager stopCapture]; |
| } |
| #endif |
| } |
| } |
| |
| SurfaceMtl::SurfaceMtl(DisplayMtl *display, |
| const egl::SurfaceState &state, |
| EGLNativeWindowType window, |
| const egl::AttributeMap &attribs) |
| : SurfaceImpl(state), mLayer((__bridge CALayer *)(window)) |
| { |
| // NOTE(hqle): Width and height attributes is ignored for now. |
| |
| // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf says that BGRA8Unorm is |
| // only supported if depth24Stencil8PixelFormatSupported capabilitiy is YES. Yet |
| // CAMetalLayer can be created with pixelFormat MTLPixelFormatBGRA8Unorm. So the mtl::Format |
| // used for SurfaceMtl is initialized a bit differently from normal TextureMtl's mtl::Format. |
| // It won't use format table, instead we initialize its values here to use BGRA8Unorm directly: |
| mColorFormat.intendedFormatId = mColorFormat.actualFormatId = angle::FormatID::B8G8R8A8_UNORM; |
| mColorFormat.metalFormat = MTLPixelFormatBGRA8Unorm; |
| |
| int depthBits = 0; |
| int stencilBits = 0; |
| if (state.config) |
| { |
| depthBits = state.config->depthSize; |
| stencilBits = state.config->stencilSize; |
| } |
| |
| if (depthBits && stencilBits) |
| { |
| if (display->getFeatures().allowSeparatedDepthStencilBuffers.enabled) |
| { |
| mDepthFormat = display->getPixelFormat(kDefaultFrameBufferDepthFormatId); |
| mStencilFormat = display->getPixelFormat(kDefaultFrameBufferStencilFormatId); |
| } |
| else |
| { |
| // We must use packed depth stencil |
| mUsePackedDepthStencil = true; |
| mDepthFormat = display->getPixelFormat(kDefaultFrameBufferDepthStencilFormatId); |
| mStencilFormat = mDepthFormat; |
| } |
| } |
| else if (depthBits) |
| { |
| mDepthFormat = display->getPixelFormat(kDefaultFrameBufferDepthFormatId); |
| } |
| else if (stencilBits) |
| { |
| mStencilFormat = display->getPixelFormat(kDefaultFrameBufferStencilFormatId); |
| } |
| } |
| |
| SurfaceMtl::~SurfaceMtl() {} |
| |
| void SurfaceMtl::destroy(const egl::Display *display) |
| { |
| mDrawableTexture = nullptr; |
| mDepthTexture = nullptr; |
| mStencilTexture = nullptr; |
| mCurrentDrawable = nil; |
| mMetalLayer = nil; |
| } |
| |
| egl::Error SurfaceMtl::initialize(const egl::Display *display) |
| { |
| DisplayMtl *displayMtl = mtl::GetImpl(display); |
| id<MTLDevice> metalDevice = displayMtl->getMetalDevice(); |
| |
| StartFrameCapture(metalDevice, displayMtl->cmdQueue().get()); |
| |
| ANGLE_MTL_OBJC_SCOPE |
| { |
| if ([mLayer isKindOfClass:CAMetalLayer.class]) |
| { |
| mMetalLayer.retainAssign(static_cast<CAMetalLayer *>(mLayer)); |
| } |
| else |
| { |
| mMetalLayer = [[[CAMetalLayer alloc] init] ANGLE_MTL_AUTORELEASE]; |
| mMetalLayer.get().frame = mLayer.frame; |
| } |
| |
| mMetalLayer.get().device = metalDevice; |
| mMetalLayer.get().pixelFormat = mColorFormat.metalFormat; |
| mMetalLayer.get().framebufferOnly = NO; // This to allow readPixels |
| |
| #if TARGET_OS_OSX || TARGET_OS_MACCATALYST |
| // Autoresize with parent layer. |
| mMetalLayer.get().autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; |
| #endif |
| |
| if (mMetalLayer.get() != mLayer) |
| { |
| mMetalLayer.get().contentsScale = mLayer.contentsScale; |
| |
| [mLayer addSublayer:mMetalLayer.get()]; |
| } |
| |
| // ensure drawableSize is set to correct value: |
| checkIfLayerResized(); |
| } |
| |
| return egl::NoError(); |
| } |
| |
| FramebufferImpl *SurfaceMtl::createDefaultFramebuffer(const gl::Context *context, |
| const gl::FramebufferState &state) |
| { |
| auto fbo = new FramebufferMtl(state, /* flipY */ true); |
| |
| return fbo; |
| } |
| |
| egl::Error SurfaceMtl::makeCurrent(const gl::Context *context) |
| { |
| angle::Result result = obtainNextDrawable(context); |
| if (result != angle::Result::Continue) |
| { |
| return egl::EglBadCurrentSurface(); |
| } |
| return egl::NoError(); |
| } |
| |
| egl::Error SurfaceMtl::unMakeCurrent(const gl::Context *context) |
| { |
| return egl::NoError(); |
| } |
| |
| egl::Error SurfaceMtl::swap(const gl::Context *context) |
| { |
| angle::Result result = swapImpl(context); |
| |
| if (result != angle::Result::Continue) |
| { |
| return egl::EglBadSurface(); |
| } |
| |
| return egl::NoError(); |
| } |
| |
| egl::Error SurfaceMtl::postSubBuffer(const gl::Context *context, |
| EGLint x, |
| EGLint y, |
| EGLint width, |
| EGLint height) |
| { |
| UNIMPLEMENTED(); |
| return egl::EglBadAccess(); |
| } |
| |
| egl::Error SurfaceMtl::querySurfacePointerANGLE(EGLint attribute, void **value) |
| { |
| UNIMPLEMENTED(); |
| return egl::EglBadAccess(); |
| } |
| |
| egl::Error SurfaceMtl::bindTexImage(const gl::Context *context, gl::Texture *texture, EGLint buffer) |
| { |
| UNIMPLEMENTED(); |
| return egl::EglBadAccess(); |
| } |
| |
| egl::Error SurfaceMtl::releaseTexImage(const gl::Context *context, EGLint buffer) |
| { |
| UNIMPLEMENTED(); |
| return egl::EglBadAccess(); |
| } |
| |
| egl::Error SurfaceMtl::getSyncValues(EGLuint64KHR *ust, EGLuint64KHR *msc, EGLuint64KHR *sbc) |
| { |
| UNIMPLEMENTED(); |
| return egl::EglBadAccess(); |
| } |
| |
| void SurfaceMtl::setSwapInterval(EGLint interval) {} |
| |
| void SurfaceMtl::setFixedWidth(EGLint width) |
| { |
| UNIMPLEMENTED(); |
| } |
| |
| void SurfaceMtl::setFixedHeight(EGLint height) |
| { |
| UNIMPLEMENTED(); |
| } |
| |
| // width and height can change with client window resizing |
| EGLint SurfaceMtl::getWidth() const |
| { |
| if (mDrawableTexture) |
| { |
| return static_cast<EGLint>(mDrawableTexture->width()); |
| } |
| |
| if (mMetalLayer) |
| { |
| return static_cast<EGLint>(mMetalLayer.get().drawableSize.width); |
| } |
| return 0; |
| } |
| |
| EGLint SurfaceMtl::getHeight() const |
| { |
| if (mDrawableTexture) |
| { |
| return static_cast<EGLint>(mDrawableTexture->height()); |
| } |
| |
| if (mMetalLayer) |
| { |
| return static_cast<EGLint>(mMetalLayer.get().drawableSize.height); |
| } |
| return 0; |
| } |
| |
| EGLint SurfaceMtl::isPostSubBufferSupported() const |
| { |
| return EGL_FALSE; |
| } |
| |
| EGLint SurfaceMtl::getSwapBehavior() const |
| { |
| return EGL_BUFFER_DESTROYED; |
| } |
| |
| angle::Result SurfaceMtl::getAttachmentRenderTarget(const gl::Context *context, |
| GLenum binding, |
| const gl::ImageIndex &imageIndex, |
| GLsizei samples, |
| FramebufferAttachmentRenderTarget **rtOut) |
| { |
| // NOTE(hqle): Support MSAA. |
| ANGLE_TRY(ensureRenderTargetsCreated(context)); |
| |
| switch (binding) |
| { |
| case GL_BACK: |
| *rtOut = &mColorRenderTarget; |
| break; |
| case GL_DEPTH: |
| *rtOut = mDepthFormat.valid() ? &mDepthRenderTarget : nullptr; |
| break; |
| case GL_STENCIL: |
| *rtOut = mStencilFormat.valid() ? &mStencilRenderTarget : nullptr; |
| break; |
| case GL_DEPTH_STENCIL: |
| // NOTE(hqle): ES 3.0 feature |
| UNREACHABLE(); |
| break; |
| } |
| |
| return angle::Result::Continue; |
| } |
| |
| angle::Result SurfaceMtl::ensureRenderTargetsCreated(const gl::Context *context) |
| { |
| if (!mDrawableTexture) |
| { |
| ANGLE_TRY(obtainNextDrawable(context)); |
| } |
| |
| return angle::Result::Continue; |
| } |
| |
| angle::Result SurfaceMtl::ensureDepthStencilSizeCorrect(const gl::Context *context, |
| gl::Framebuffer::DirtyBits *fboDirtyBits) |
| { |
| ASSERT(mDrawableTexture && mDrawableTexture->get()); |
| |
| ContextMtl *contextMtl = mtl::GetImpl(context); |
| auto size = mDrawableTexture->size(); |
| |
| if (mDepthFormat.valid() && (!mDepthTexture || mDepthTexture->size() != size)) |
| { |
| ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, mDepthFormat, size.width, size.height, 1, |
| true, false, &mDepthTexture)); |
| |
| mDepthRenderTarget.set(mDepthTexture, 0, 0, mDepthFormat); |
| fboDirtyBits->set(gl::Framebuffer::DIRTY_BIT_DEPTH_ATTACHMENT); |
| } |
| |
| if (mStencilFormat.valid() && (!mStencilTexture || mStencilTexture->size() != size)) |
| { |
| if (mUsePackedDepthStencil) |
| { |
| mStencilTexture = mDepthTexture; |
| } |
| else |
| { |
| ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, mStencilFormat, size.width, |
| size.height, 1, true, false, &mStencilTexture)); |
| } |
| |
| mStencilRenderTarget.set(mStencilTexture, 0, 0, mStencilFormat); |
| fboDirtyBits->set(gl::Framebuffer::DIRTY_BIT_STENCIL_ATTACHMENT); |
| } |
| |
| return angle::Result::Continue; |
| } |
| |
| void SurfaceMtl::checkIfLayerResized() |
| { |
| CGSize currentDrawableSize = mMetalLayer.get().drawableSize; |
| CGSize currentLayerSize = mMetalLayer.get().bounds.size; |
| CGFloat currentLayerContentsScale = mMetalLayer.get().contentsScale; |
| CGSize expectedDrawableSize = CGSizeMake(currentLayerSize.width * currentLayerContentsScale, |
| currentLayerSize.height * currentLayerContentsScale); |
| if (currentDrawableSize.width != expectedDrawableSize.width || |
| currentDrawableSize.height != expectedDrawableSize.height) |
| { |
| // Resize the internal drawable texture. |
| mMetalLayer.get().drawableSize = expectedDrawableSize; |
| } |
| } |
| |
| angle::Result SurfaceMtl::obtainNextDrawable(const gl::Context *context) |
| { |
| checkIfLayerResized(); |
| |
| ANGLE_MTL_OBJC_SCOPE |
| { |
| ContextMtl *contextMtl = mtl::GetImpl(context); |
| |
| StartFrameCapture(contextMtl); |
| |
| ANGLE_MTL_TRY(contextMtl, mMetalLayer); |
| |
| if (mDrawableTexture) |
| { |
| mDrawableTexture->set(nil); |
| } |
| |
| mCurrentDrawable = nil; |
| mCurrentDrawable.retainAssign([mMetalLayer nextDrawable]); |
| if (!mCurrentDrawable) |
| { |
| // The GPU might be taking too long finishing its rendering to the previous frame. |
| // Try again, indefinitely wait until the previous frame render finishes. |
| // TODO: this may wait forever here |
| mMetalLayer.get().allowsNextDrawableTimeout = NO; |
| mCurrentDrawable.retainAssign([mMetalLayer nextDrawable]); |
| mMetalLayer.get().allowsNextDrawableTimeout = YES; |
| } |
| |
| if (!mDrawableTexture) |
| { |
| mDrawableTexture = mtl::Texture::MakeFromMetal(mCurrentDrawable.get().texture); |
| mColorRenderTarget.set(mDrawableTexture, 0, 0, mColorFormat); |
| } |
| else |
| { |
| mDrawableTexture->set(mCurrentDrawable.get().texture); |
| } |
| |
| ANGLE_MTL_LOG("Current metal drawable size=%d,%d", mDrawableTexture->width(), |
| mDrawableTexture->height()); |
| |
| gl::Framebuffer::DirtyBits fboDirtyBits; |
| fboDirtyBits.set(gl::Framebuffer::DIRTY_BIT_COLOR_ATTACHMENT_0); |
| |
| // Now we have to resize depth stencil buffers if necessary. |
| ANGLE_TRY(ensureDepthStencilSizeCorrect(context, &fboDirtyBits)); |
| |
| // Need to notify default framebuffer to invalidate its render targets. |
| // Since a new drawable texture has been obtained, also, the depth stencil |
| // buffers might have been resized. |
| gl::Framebuffer *defaultFbo = |
| context->getFramebuffer(gl::Framebuffer::kDefaultDrawFramebufferHandle); |
| if (defaultFbo) |
| { |
| FramebufferMtl *framebufferMtl = mtl::GetImpl(defaultFbo); |
| ANGLE_TRY(framebufferMtl->syncState(context, fboDirtyBits)); |
| } |
| |
| return angle::Result::Continue; |
| } |
| } |
| |
| angle::Result SurfaceMtl::swapImpl(const gl::Context *context) |
| { |
| ANGLE_TRY(ensureRenderTargetsCreated(context)); |
| |
| ContextMtl *contextMtl = mtl::GetImpl(context); |
| |
| contextMtl->present(context, mCurrentDrawable); |
| |
| StopFrameCapture(); |
| |
| ANGLE_TRY(obtainNextDrawable(context)); |
| |
| return angle::Result::Continue; |
| } |
| } |