// // Copyright 2023 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. // // mtl_pipeline_cache.mm: // Defines classes for caching of mtl pipelines // #include "libANGLE/renderer/metal/mtl_pipeline_cache.h" #include "libANGLE/renderer/metal/ContextMtl.h" namespace rx { namespace mtl { namespace { bool HasDefaultAttribs(const RenderPipelineDesc &rpdesc) { const VertexDesc &desc = rpdesc.vertexDescriptor; for (uint8_t i = 0; i < desc.numAttribs; ++i) { if (desc.attributes[i].bufferIndex == kDefaultAttribsBindingIndex) { return true; } } return false; } bool HasValidRenderTarget(const mtl::ContextDevice &device, const MTLRenderPipelineDescriptor *descriptor) { const NSUInteger maxColorRenderTargets = GetMaxNumberOfRenderTargetsForDevice(device); for (NSUInteger i = 0; i < maxColorRenderTargets; ++i) { auto colorAttachment = descriptor.colorAttachments[i]; if (colorAttachment && colorAttachment.pixelFormat != MTLPixelFormatInvalid) { return true; } } if (descriptor.depthAttachmentPixelFormat != MTLPixelFormatInvalid) { return true; } if (descriptor.stencilAttachmentPixelFormat != MTLPixelFormatInvalid) { return true; } return false; } angle::Result ValidateRenderPipelineState(ContextMtl *context, const MTLRenderPipelineDescriptor *descriptor) { const mtl::ContextDevice &device = context->getMetalDevice(); if (!context->getDisplay()->getFeatures().allowRenderpassWithoutAttachment.enabled && !HasValidRenderTarget(device, descriptor)) { ANGLE_MTL_HANDLE_ERROR( context, "Render pipeline requires at least one render target for this device.", GL_INVALID_OPERATION); return angle::Result::Stop; } // Ensure the device can support the storage requirement for render targets. if (DeviceHasMaximumRenderTargetSize(device)) { NSUInteger maxSize = GetMaxRenderTargetSizeForDeviceInBytes(device); NSUInteger renderTargetSize = ComputeTotalSizeUsedForMTLRenderPipelineDescriptor(descriptor, context, device); if (renderTargetSize > maxSize) { std::stringstream errorStream; errorStream << "This set of render targets requires " << renderTargetSize << " bytes of pixel storage. This device supports " << maxSize << " bytes."; ANGLE_MTL_HANDLE_ERROR(context, errorStream.str().c_str(), GL_INVALID_OPERATION); return angle::Result::Stop; } } return angle::Result::Continue; } angle::Result CreateRenderPipelineState(ContextMtl *context, const PipelineKey &key, AutoObjCPtr> *outRenderPipeline) { ANGLE_MTL_OBJC_SCOPE { ASSERT(key.isRenderPipeline()); if (!key.vertexShader) { // Render pipeline without vertex shader is invalid. ANGLE_MTL_HANDLE_ERROR(context, "Render pipeline without vertex shader is invalid.", GL_INVALID_OPERATION); return angle::Result::Stop; } const mtl::ContextDevice &metalDevice = context->getMetalDevice(); auto objCDesc = key.pipelineDesc.createMetalDesc(key.vertexShader, key.fragmentShader); ANGLE_TRY(ValidateRenderPipelineState(context, objCDesc)); // Special attribute slot for default attribute if (HasDefaultAttribs(key.pipelineDesc)) { auto defaultAttribLayoutObjCDesc = adoptObjCObj( [[MTLVertexBufferLayoutDescriptor alloc] init]); defaultAttribLayoutObjCDesc.get().stepFunction = MTLVertexStepFunctionConstant; defaultAttribLayoutObjCDesc.get().stepRate = 0; defaultAttribLayoutObjCDesc.get().stride = kDefaultAttributeSize * kMaxVertexAttribs; [objCDesc.get().vertexDescriptor.layouts setObject:defaultAttribLayoutObjCDesc atIndexedSubscript:kDefaultAttribsBindingIndex]; } // Create pipeline state NSError *err = nil; auto newState = metalDevice.newRenderPipelineStateWithDescriptor(objCDesc, &err); if (err) { ANGLE_MTL_HANDLE_ERROR(context, mtl::FormatMetalErrorMessage(err).c_str(), GL_INVALID_OPERATION); return angle::Result::Stop; } *outRenderPipeline = newState; return angle::Result::Continue; } } angle::Result CreateComputePipelineState( ContextMtl *context, const PipelineKey &key, AutoObjCPtr> *outComputePipeline) { ANGLE_MTL_OBJC_SCOPE { ASSERT(!key.isRenderPipeline()); if (!key.computeShader) { ANGLE_MTL_HANDLE_ERROR(context, "Compute pipeline without a shader is invalid.", GL_INVALID_OPERATION); return angle::Result::Stop; } const mtl::ContextDevice &metalDevice = context->getMetalDevice(); // Create pipeline state NSError *err = nil; auto newState = metalDevice.newComputePipelineStateWithFunction(key.computeShader, &err); if (err) { ANGLE_MTL_HANDLE_ERROR(context, mtl::FormatMetalErrorMessage(err).c_str(), GL_INVALID_OPERATION); return angle::Result::Stop; } *outComputePipeline = newState; return angle::Result::Continue; } } } // namespace bool PipelineKey::isRenderPipeline() const { if (vertexShader) { ASSERT(!computeShader); return true; } else { ASSERT(computeShader); return false; } } bool PipelineKey::operator==(const PipelineKey &rhs) const { if (isRenderPipeline() != rhs.isRenderPipeline()) { return false; } if (isRenderPipeline()) { return std::tie(vertexShader, fragmentShader, pipelineDesc) == std::tie(rhs.vertexShader, rhs.fragmentShader, rhs.pipelineDesc); } else { return computeShader == rhs.computeShader; } } size_t PipelineKey::hash() const { if (isRenderPipeline()) { return angle::HashMultiple(vertexShader.get(), fragmentShader.get(), pipelineDesc); } else { return angle::HashMultiple(computeShader.get()); } } PipelineCache::PipelineCache() : mPipelineCache(kMaxPipelines) {} angle::Result PipelineCache::getRenderPipeline( ContextMtl *context, id vertexShader, id fragmentShader, const RenderPipelineDesc &desc, AutoObjCPtr> *outRenderPipeline) { PipelineKey key; key.vertexShader.retainAssign(vertexShader); key.fragmentShader.retainAssign(fragmentShader); key.pipelineDesc = desc; auto iter = mPipelineCache.Get(key); if (iter != mPipelineCache.end()) { // Should be no way that this key matched a compute pipeline entry ASSERT(iter->second.renderPipeline); *outRenderPipeline = iter->second.renderPipeline; return angle::Result::Continue; } angle::TrimCache(kMaxPipelines, kGCLimit, "render pipeline", &mPipelineCache); PipelineVariant newPipeline; ANGLE_TRY(CreateRenderPipelineState(context, key, &newPipeline.renderPipeline)); iter = mPipelineCache.Put(std::move(key), std::move(newPipeline)); *outRenderPipeline = iter->second.renderPipeline; return angle::Result::Continue; } angle::Result PipelineCache::getComputePipeline( ContextMtl *context, id computeShader, AutoObjCPtr> *outComputePipeline) { PipelineKey key; key.computeShader.retainAssign(computeShader); auto iter = mPipelineCache.Get(key); if (iter != mPipelineCache.end()) { // Should be no way that this key matched a render pipeline entry ASSERT(iter->second.computePipeline); *outComputePipeline = iter->second.computePipeline; return angle::Result::Continue; } angle::TrimCache(kMaxPipelines, kGCLimit, "render pipeline", &mPipelineCache); PipelineVariant newPipeline; ANGLE_TRY(CreateComputePipelineState(context, key, &newPipeline.computePipeline)); iter = mPipelineCache.Put(std::move(key), std::move(newPipeline)); *outComputePipeline = iter->second.computePipeline; return angle::Result::Continue; } } // namespace mtl } // namespace rx