#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace mbgl { namespace mtl { // Maximum number of vertex attributes, per vertex descriptor // 31 for Apple2-8, Mac2, per https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf constexpr uint32_t maximumVertexBindingCount = 31; Context::Context(RendererBackend& backend_) : gfx::Context(mtl::maximumVertexBindingCount), backend(backend_) {} Context::~Context() noexcept { if (cleanupOnDestruction) { performCleanup(); emptyVertexBuffer.reset(); tileVertexBuffer.reset(); tileIndexBuffer.reset(); clipMaskShader.reset(); clipMaskDepthStencilState.reset(); clipMaskPipelineState.reset(); clipMaskUniformsBuffer.reset(); stencilStateRenderable = nullptr; #if !defined(NDEBUG) Log::Debug(Event::General, "Rendering Stats:\n" + stats.toString("\n")); #endif assert(stats.isZero()); } } std::unique_ptr Context::createCommandEncoder() { return std::make_unique(*this); } BufferResource Context::createBuffer( const void* data, std::size_t size, gfx::BufferUsageType, bool isIndexBuffer, bool persistent) const { return {const_cast(*this), data, size, MTL::ResourceStorageModeShared, isIndexBuffer, persistent}; } UniqueShaderProgram Context::createProgram(std::string name, const std::string_view source, const std::string_view vertexName, const std::string_view fragmentName, const ProgramParameters& programParameters, const std::unordered_map& additionalDefines) { const auto pool = NS::TransferPtr(NS::AutoreleasePool::alloc()->init()); // No NSMutableDictionary? const auto& programDefines = programParameters.getDefines(); const auto numDefines = programDefines.size() + additionalDefines.size(); std::vector rawDefines; rawDefines.reserve(2 * numDefines); const auto addDefine = [&rawDefines](const auto& pair) { auto* nsKey = NS::String::string(pair.first.data(), NS::UTF8StringEncoding); auto* nsVal = NS::String::string(pair.second.data(), NS::UTF8StringEncoding); rawDefines.insert(std::next(rawDefines.begin(), rawDefines.size() / 2), nsKey); rawDefines.insert(rawDefines.end(), nsVal); }; std::for_each(programDefines.begin(), programDefines.end(), addDefine); std::for_each(additionalDefines.begin(), additionalDefines.end(), addDefine); const auto nsDefines = NS::Dictionary::dictionary( &rawDefines[numDefines], rawDefines.data(), static_cast(numDefines)); rawDefines.clear(); auto options = NS::TransferPtr(MTL::CompileOptions::alloc()->init()); options->setPreprocessorMacros(nsDefines); options->setFastMathEnabled(true); options->setLanguageVersion(MTL::LanguageVersion2_1); // TODO: Compile common code into a `LibraryTypeDynamic` to be used by other shaders // instead of duplicating that code in each and every shader compilation. options->setLibraryType(MTL::LibraryTypeExecutable); // Allows use of the [[invariant]] attribute on position outputs to // guarantee that the GPU performs the calculations the same way. options->setPreserveInvariance(true); // TODO: Allow use of `LibraryOptimizationLevelSize` which "may also reduce compile time" // requires a check for iOS 16+ // options->setOptimizationLevel(MTL::LibraryOptimizationLevelDefault); NS::Error* error = nullptr; NS::String* nsSource = NS::String::string(source.data(), NS::UTF8StringEncoding); const auto& device = backend.getDevice(); MTL::Library* library = device->newLibrary(nsSource, options.get(), &error); if (!library || error) { const auto errPtr = error ? error->localizedDescription()->utf8String() : nullptr; const auto errStr = (errPtr && errPtr[0]) ? ": " + std::string(errPtr) : std::string(); Log::Error(Event::Shader, name + " compile failed" + errStr); assert(false); return nullptr; } const auto nsVertName = NS::String::string(vertexName.data(), NS::UTF8StringEncoding); NS::SharedPtr vertexFunction = NS::RetainPtr(library->newFunction(nsVertName)); if (!vertexFunction) { Log::Error(Event::Shader, name + " missing vertex function " + vertexName.data()); assert(false); return nullptr; } // fragment function is optional NS::SharedPtr fragmentFunction; if (!fragmentName.empty()) { const auto nsFragName = NS::String::string(fragmentName.data(), NS::UTF8StringEncoding); fragmentFunction = NS::RetainPtr(library->newFunction(nsFragName)); if (!fragmentFunction) { Log::Error(Event::Shader, name + " missing fragment function " + fragmentName.data()); assert(false); return nullptr; } } return std::make_unique( std::move(name), backend, std::move(vertexFunction), std::move(fragmentFunction)); } MTLTexturePtr Context::createMetalTexture(MTLTextureDescriptorPtr textureDescriptor) const { return NS::TransferPtr(backend.getDevice()->newTexture(textureDescriptor.get())); } MTLSamplerStatePtr Context::createMetalSamplerState(MTLSamplerDescriptorPtr samplerDescriptor) const { return NS::TransferPtr(backend.getDevice()->newSamplerState(samplerDescriptor.get())); } void Context::performCleanup() { stats.numDrawCalls = 0; stats.numFrames++; clipMaskUniformsBufferUsed = false; } gfx::UniqueDrawableBuilder Context::createDrawableBuilder(std::string name) { return std::make_unique(std::move(name)); } gfx::UniformBufferPtr Context::createUniformBuffer(const void* data, std::size_t size, bool persistent) { return std::make_shared( createBuffer(data, size, gfx::BufferUsageType::StaticDraw, /*isIndexBuffer=*/false, persistent)); } gfx::ShaderProgramBasePtr Context::getGenericShader(gfx::ShaderRegistry& shaders, const std::string& name) { const auto shaderGroup = shaders.getShaderGroup(name); auto shader = shaderGroup ? shaderGroup->getOrCreateShader(*this, {}) : gfx::ShaderProgramBasePtr{}; return std::static_pointer_cast(std::move(shader)); } TileLayerGroupPtr Context::createTileLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) { return std::make_shared(layerIndex, initialCapacity, std::move(name)); } LayerGroupPtr Context::createLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) { return std::make_shared(layerIndex, initialCapacity, name); } gfx::Texture2DPtr Context::createTexture2D() { return std::make_shared(*this); } RenderTargetPtr Context::createRenderTarget(const Size size, const gfx::TextureChannelDataType type) { return std::make_shared(*this, size, type); } void Context::resetState(gfx::DepthMode depthMode, gfx::ColorMode colorMode) {} bool Context::emplaceOrUpdateUniformBuffer(gfx::UniformBufferPtr& buffer, const void* data, std::size_t size, bool persistent) { if (buffer) { buffer->update(data, size); return false; } else { buffer = createUniformBuffer(data, size, persistent); return true; } } const BufferResource& Context::getTileVertexBuffer() { if (!tileVertexBuffer) { const auto vertices = RenderStaticData::tileVertices(); constexpr auto vertexSize = sizeof(decltype(vertices)::Vertex::a1); tileVertexBuffer.emplace(createBuffer(vertices.data(), vertices.bytes(), gfx::BufferUsageType::StaticDraw, /*isIndexBuffer=*/true, /*persistent=*/true)); } return *tileVertexBuffer; } const BufferResource& Context::getTileIndexBuffer() { if (!tileIndexBuffer) { const auto indexes = RenderStaticData::quadTriangleIndices(); tileIndexBuffer.emplace(createBuffer(indexes.data(), indexes.bytes(), gfx::BufferUsageType::StaticDraw, /*isIndexBuffer=*/true, /*persistent=*/true)); } return *tileIndexBuffer; } const UniqueVertexBufferResource& Context::getEmptyVertexBuffer() { if (!emptyVertexBuffer) { // This buffer is bound to vertex attribtue indexes when the uniforms are used instead and // shaders are expected not to access the attribute values, but Metal requires a binding. // This buffer could also hold a single default value applied for all vertices, in which case // it could not be shared (See `MTL::VertexStepFunctionConstant` in the vertex attribute // descriptors, but that isn't currently being used. constexpr std::size_t size = 32; constexpr auto usage = gfx::BufferUsageType::StaticDraw; emptyVertexBuffer = std::make_unique( createBuffer(nullptr, size, usage, /*isIndexBuffer=*/false, /*persistent=*/false)); } return emptyVertexBuffer; } namespace { const auto clipMaskStencilMode = gfx::StencilMode{ /*.test=*/gfx::StencilMode::Always(), /*.ref=*/0, /*.mask=*/0b11111111, /*.fail=*/gfx::StencilOpType::Keep, /*.depthFail=*/gfx::StencilOpType::Keep, /*.pass=*/gfx::StencilOpType::Replace, }; const auto clipMaskDepthMode = gfx::DepthMode{ /*.func=*/gfx::DepthFunctionType::Always, /*.mask=*/gfx::DepthMaskType::ReadOnly, /*.range=*/{0, 1}, }; } // namespace bool Context::renderTileClippingMasks(gfx::RenderPass& renderPass, RenderStaticData& staticData, const std::vector& tileUBOs) { using ShaderClass = shaders::ShaderSource; if (!clipMaskShader) { const auto group = staticData.shaders->getShaderGroup("ClippingMaskProgram"); if (group) { clipMaskShader = std::static_pointer_cast(group->getOrCreateShader(*this, {})); } } if (!clipMaskShader) { assert(!"Failed to create shader for clip masking"); return false; } const auto& mtlShader = static_cast(*clipMaskShader); auto& mtlRenderPass = static_cast(renderPass); const auto& encoder = mtlRenderPass.getMetalEncoder(); const auto colorMode = gfx::ColorMode::disabled(); // Create a vertex buffer from the fixed tile coordinates constexpr auto vertexSize = sizeof(gfx::Vertex::a1); const auto& vertexRes = getTileVertexBuffer(); if (!vertexRes) { assert(vertexSize == vertexRes.getSizeInBytes()); return false; } // Create a buffer from the fixed tile indexes constexpr NS::UInteger indexCount = 6; const auto indexRes = &getTileIndexBuffer(); if (!indexRes) { return false; } const auto& renderPassDescriptor = mtlRenderPass.getDescriptor(); const auto& renderable = renderPassDescriptor.renderable; if (stencilStateRenderable != &renderable) { // We're on a new renderable, invalidate objects constructed for the previous one. clipMaskPipelineState.reset(); clipMaskDepthStencilState.reset(); stencilStateRenderable = &renderable; } // Create the depth-stencil state, if necessary. if (!clipMaskDepthStencilState) { if (auto depthStencilState = makeDepthStencilState(clipMaskDepthMode, clipMaskStencilMode, renderable)) { clipMaskDepthStencilState = std::move(depthStencilState); } } if (clipMaskDepthStencilState) { encoder->setDepthStencilState(clipMaskDepthStencilState.get()); } else { assert(!"Failed to create depth-stencil state for clip masking"); return false; } if (!clipMaskPipelineState) { // A vertex descriptor tells Metal what's in the vertex buffer auto vertDesc = NS::RetainPtr(MTL::VertexDescriptor::vertexDescriptor()); auto attribDesc = NS::TransferPtr(MTL::VertexAttributeDescriptor::alloc()->init()); auto layoutDesc = NS::TransferPtr(MTL::VertexBufferLayoutDescriptor::alloc()->init()); if (!vertDesc || !attribDesc || !layoutDesc) { return false; } attribDesc->setBufferIndex(ShaderClass::attributes[0].index); attribDesc->setOffset(0); attribDesc->setFormat(MTL::VertexFormatShort2); layoutDesc->setStride(static_cast(vertexSize)); layoutDesc->setStepFunction(MTL::VertexStepFunctionPerVertex); layoutDesc->setStepRate(1); vertDesc->attributes()->setObject(attribDesc.get(), 0); vertDesc->layouts()->setObject(layoutDesc.get(), 0); // Create a render pipeline state, telling Metal how to render the primitives const auto& renderPassDescriptor = mtlRenderPass.getDescriptor(); if (auto state = mtlShader.getRenderPipelineState(renderable, vertDesc, colorMode)) { clipMaskPipelineState = std::move(state); } } if (clipMaskPipelineState) { encoder->setRenderPipelineState(clipMaskPipelineState.get()); } else { assert(!"Failed to create render pipeline state for clip masking"); return false; } // Create a buffer for the UBO data. constexpr auto uboSize = sizeof(shaders::ClipUBO); const auto bufferSize = tileUBOs.size() * uboSize; std::optional tempBuffer; auto& uboBuffer = clipMaskUniformsBufferUsed ? tempBuffer : clipMaskUniformsBuffer; clipMaskUniformsBufferUsed = true; if (!uboBuffer || !*uboBuffer || uboBuffer->getSizeInBytes() < bufferSize) { uboBuffer = createBuffer(tileUBOs.data(), bufferSize, gfx::BufferUsageType::StaticDraw, /*isIndexBuffer=*/false, /*persistent=*/!clipMaskUniformsBufferUsed); if (!uboBuffer) { return false; } } else { uboBuffer->update(tileUBOs.data(), bufferSize, /*offset=*/0); } encoder->setCullMode(MTL::CullModeNone); mtlRenderPass.bindVertex(vertexRes, /*offset=*/0, ShaderClass::attributes[0].index); // Instancing is disabled for now because the `[[stencil]]` attribute in the fragment shader output // that we need to apply a different stencil value for each tile causes a problem on some older (A8-A11) // devices, specifically `XPC_ERROR_CONNECTION_INTERRUPTED` when creating a render pipeline state. // Adding a `[[depth(...)]]` output to the shader prevents this error, but the stencil value is // still not written to the stencil attachment on those same devices. #if STENCIL_INSTANCING encoder->setVertexBuffer(uboBuffer.getMetalBuffer().get(), /*offset=*/0, ShaderClass::uniforms[0].index); encoder->drawIndexedPrimitives(MTL::PrimitiveType::PrimitiveTypeTriangle, indexCount, MTL::IndexType::IndexTypeUInt16, indexRes->getMetalBuffer().get(), /*indexOffset=*/0, /*instanceCount=*/static_cast(tileUBOs.size()), /*baseVertex=*/0, /*baseInstance=*/0); #else const auto uboIndex = ShaderClass::uniforms[0].index; for (std::size_t ii = 0; ii < tileUBOs.size(); ++ii) { encoder->setStencilReferenceValue(tileUBOs[ii].stencil_ref); mtlRenderPass.bindVertex(*uboBuffer, /*offset=*/ii * uboSize, uboIndex, /*size=*/uboSize); encoder->drawIndexedPrimitives(MTL::PrimitiveType::PrimitiveTypeTriangle, indexCount, MTL::IndexType::IndexTypeUInt16, indexRes->getMetalBuffer().get(), /*indexOffset=*/0, /*instanceCount=*/1, /*baseVertex=*/0, /*baseInstance=*/0); } #endif stats.numDrawCalls++; stats.totalDrawCalls++; return true; } void Context::setDirtyState() {} std::unique_ptr Context::createOffscreenTexture(Size size, gfx::TextureChannelDataType type, bool depth, bool stencil) { return std::make_unique(*this, size, type, depth, stencil); } std::unique_ptr Context::createOffscreenTexture(Size size, gfx::TextureChannelDataType type) { return createOffscreenTexture(size, type, false, false); } std::unique_ptr Context::createTextureResource(Size, gfx::TexturePixelType, gfx::TextureChannelDataType) { assert(false); return nullptr; } std::unique_ptr Context::createRenderbufferResource(gfx::RenderbufferPixelType, Size size) { return std::make_unique(); } std::unique_ptr Context::createDrawScopeResource() { assert(false); return nullptr; } #if !defined(NDEBUG) void Context::visualizeStencilBuffer() {} void Context::visualizeDepthBuffer(float depthRangeSize) {} #endif // !defined(NDEBUG) void Context::clearStencilBuffer(int32_t) { // See `PaintParameters::clearStencil` assert(false); } namespace { MTL::CompareFunction mapFunc(const gfx::DepthFunctionType func) { switch (func) { default: case gfx::DepthFunctionType::Never: return MTL::CompareFunction::CompareFunctionNever; case gfx::DepthFunctionType::Less: return MTL::CompareFunction::CompareFunctionLess; case gfx::DepthFunctionType::Equal: return MTL::CompareFunction::CompareFunctionEqual; case gfx::DepthFunctionType::LessEqual: return MTL::CompareFunction::CompareFunctionLessEqual; case gfx::DepthFunctionType::Greater: return MTL::CompareFunction::CompareFunctionGreater; case gfx::DepthFunctionType::NotEqual: return MTL::CompareFunction::CompareFunctionNotEqual; case gfx::DepthFunctionType::GreaterEqual: return MTL::CompareFunction::CompareFunctionGreaterEqual; case gfx::DepthFunctionType::Always: return MTL::CompareFunction::CompareFunctionAlways; }; } MTL::CompareFunction mapFunc(const gfx::StencilFunctionType func) { switch (func) { default: case gfx::StencilFunctionType::Never: return MTL::CompareFunction::CompareFunctionNever; case gfx::StencilFunctionType::Less: return MTL::CompareFunction::CompareFunctionLess; case gfx::StencilFunctionType::Equal: return MTL::CompareFunction::CompareFunctionEqual; case gfx::StencilFunctionType::LessEqual: return MTL::CompareFunction::CompareFunctionLessEqual; case gfx::StencilFunctionType::Greater: return MTL::CompareFunction::CompareFunctionGreater; case gfx::StencilFunctionType::NotEqual: return MTL::CompareFunction::CompareFunctionNotEqual; case gfx::StencilFunctionType::GreaterEqual: return MTL::CompareFunction::CompareFunctionGreaterEqual; case gfx::StencilFunctionType::Always: return MTL::CompareFunction::CompareFunctionAlways; }; } MTL::StencilOperation mapOperation(const gfx::StencilOpType op) { switch (op) { case gfx::StencilOpType::Zero: return MTL::StencilOperation::StencilOperationZero; default: case gfx::StencilOpType::Keep: return MTL::StencilOperation::StencilOperationKeep; case gfx::StencilOpType::Replace: return MTL::StencilOperation::StencilOperationReplace; case gfx::StencilOpType::Increment: return MTL::StencilOperation::StencilOperationIncrementClamp; case gfx::StencilOpType::Decrement: return MTL::StencilOperation::StencilOperationDecrementClamp; case gfx::StencilOpType::Invert: return MTL::StencilOperation::StencilOperationInvert; case gfx::StencilOpType::IncrementWrap: return MTL::StencilOperation::StencilOperationIncrementWrap; case gfx::StencilOpType::DecrementWrap: return MTL::StencilOperation::StencilOperationDecrementWrap; } } void applyDepthMode(const gfx::DepthMode& depthMode, MTL::DepthStencilDescriptor* desc) { desc->setDepthCompareFunction(mapFunc(depthMode.func)); desc->setDepthWriteEnabled(depthMode.mask == gfx::DepthMaskType::ReadWrite); // depthMode.range ? } struct StencilModeVisitor { MTL::StencilDescriptor* desc; template void operator()(const gfx::StencilMode::SimpleTest& mode) { apply(mode.func, std::numeric_limits::max()); } template void operator()(const gfx::StencilMode::MaskedTest& mode) { apply(mode.func, mode.mask); } void apply(gfx::StencilFunctionType func, uint32_t mask) { desc->setStencilCompareFunction(mapFunc(func)); desc->setReadMask(mask); desc->setWriteMask(mask); } }; // helper type for the visitor #4 void applyStencilMode(const gfx::StencilMode& stencilMode, MTL::StencilDescriptor* desc) { mapbox::util::apply_visitor(StencilModeVisitor{desc}, stencilMode.test); desc->setStencilFailureOperation(mapOperation(stencilMode.fail)); desc->setDepthFailureOperation(mapOperation(stencilMode.depthFail)); desc->setDepthStencilPassOperation(mapOperation(stencilMode.pass)); } } // namespace MTLDepthStencilStatePtr Context::makeDepthStencilState(const gfx::DepthMode& depthMode, const gfx::StencilMode& stencilMode, const gfx::Renderable& renderable) const { auto depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init()); if (!depthStencilDescriptor) { return {}; } auto& device = backend.getDevice(); const auto& renderableResource = renderable.getResource(); if (const auto& rpd = renderableResource.getRenderPassDescriptor()) { // Setting depth/stencil properties when the corresponding target textures aren't set causes, e.g.: // `Draw Errors Validation MTLDepthStencilDescriptor sets depth test but MTLRenderPassDescriptor has a nil // depthAttachment texture` if (auto* depthTarget = rpd->depthAttachment()) { if (auto* tex = depthTarget->texture()) { applyDepthMode(depthMode, depthStencilDescriptor.get()); } } if (auto* stencilTarget = rpd->stencilAttachment()) { if (auto* tex = stencilTarget->texture()) { auto stencilDescriptor = NS::TransferPtr(MTL::StencilDescriptor::alloc()->init()); if (!stencilDescriptor) { return {}; } applyStencilMode(stencilMode, stencilDescriptor.get()); depthStencilDescriptor->setFrontFaceStencil(stencilDescriptor.get()); depthStencilDescriptor->setBackFaceStencil(stencilDescriptor.get()); } } } return NS::TransferPtr(device->newDepthStencilState(depthStencilDescriptor.get())); } } // namespace mtl } // namespace mbgl