From 83b17f96f41f9e06439eecb2e4cf8097cd60f338 Mon Sep 17 00:00:00 2001 From: navjack Date: Tue, 15 Sep 2026 00:18:27 -0400 Subject: [PATCH] Metal: keep render passes across framebuffer rebinds; cache partial-clear state setFramebuffer ended the active render encoder even when the same framebuffer was rebound. Renderers that set the framebuffer before every draw started a new render pass per draw, which dominated encoding time on Apple GPUs. Return early when the target is unchanged, an encoder is open and no clears are pending, matching the Vulkan backend's guard. Partial color clears created a new depth-stencil state on every clear and never released it; use a device-lifetime clearColorDepthState instead. Both partial clear paths now compute the clear pipeline key from the framebuffer formats and look up the cached pipeline before allocating a render pipeline descriptor, which is only built on a cache miss. Co-Authored-By: Claude Opus 5 --- plume_metal.cpp | 137 +++++++++++++++++++++++++++++++++++------------- plume_metal.h | 3 ++ 2 files changed, 105 insertions(+), 35 deletions(-) diff --git a/plume_metal.cpp b/plume_metal.cpp index 373c87e..dc15b88 100644 --- a/plume_metal.cpp +++ b/plume_metal.cpp @@ -68,6 +68,30 @@ namespace plume { return key.value; } + // Same key as createClearPipelineKey, built from the formats a clear will use, so the cached + // pipeline can be found without allocating a render pipeline descriptor for every clear. + uint64_t clearPipelineKey(bool depthWriteEnabled, bool stencilWriteEnabled, uint32_t sampleCount, + const MTL::PixelFormat *colorFormats, uint32_t colorFormatCount, MTL::PixelFormat depthFormat) { + auto colorFormat = [&](uint32_t index) { + return (index < colorFormatCount) ? static_cast(mapRenderFormat(colorFormats[index])) : 0llu; + }; + + ClearPipelineKey key; + key.value = 0; + key.depthClear = depthWriteEnabled ? 1 : 0; + key.stencilClear = stencilWriteEnabled ? 1 : 0; + key.msaaCount = sampleCount; + key.colorFormat0 = colorFormat(0); + key.colorFormat1 = colorFormat(1); + key.colorFormat2 = colorFormat(2); + key.colorFormat3 = colorFormat(3); + key.colorFormat4 = colorFormat(4); + key.colorFormat5 = colorFormat(5); + key.colorFormat6 = colorFormat(6); + key.depthFormat = static_cast(mapRenderFormat(depthFormat)); + return key.value; + } + NS::UInteger alignmentForRenderFormat(MTL::Device *device, RenderFormat format) { const auto deviceAlignment = device->minimumLinearTextureAlignmentForPixelFormat(mapPixelFormat(format)); @@ -2725,6 +2749,14 @@ namespace plume { } void MetalCommandList::setFramebuffer(const RenderFramebuffer *framebuffer) { + // Rebinding the target of the open render encoder is a no-op. Ending the encoder here would + // start a new render pass per draw for callers that set the framebuffer before every draw, + // and render pass creation dominates encoding time on Apple GPUs. + if ((framebuffer != nullptr) && (framebuffer == targetFramebuffer) && (activeType == EncoderType::Render) && + (activeRenderEncoder != nullptr) && !pendingClears.active) { + return; + } + MetalAutoreleasePool releasePool; endOtherEncoders(EncoderType::Render); endActiveRenderEncoder(); @@ -2797,34 +2829,45 @@ namespace plume { // Process clears activeRenderEncoder->pushDebugGroup(MTLSTR("ColorClear")); - MTL::RenderPipelineDescriptor* pipelineDesc = MTL::RenderPipelineDescriptor::alloc()->init(); - pipelineDesc->setVertexFunction(device->clearVertexFunction); - pipelineDesc->setFragmentFunction(device->clearColorFunction); - pipelineDesc->setRasterSampleCount(targetFramebuffer->colorAttachments[attachmentIndex].sampleCount); + const bool hasDepth = targetFramebuffer->depthAttachment.format != RenderFormat::UNKNOWN; + const MTL::PixelFormat depthPixelFormat = hasDepth ? targetFramebuffer->depthAttachment.getTexture()->pixelFormat() : MTL::PixelFormatInvalid; + if (hasDepth) { + // Cached device state; creating one here per clear leaked a depth-stencil state each time. + activeRenderEncoder->setDepthStencilState(device->clearColorDepthState); + } + + // Look up the cached pipeline before allocating a descriptor; only a cache miss builds one. + MTL::PixelFormat colorFormats[7] = {}; + if (attachmentIndex < 7) { + colorFormats[attachmentIndex] = targetFramebuffer->colorAttachments[attachmentIndex].getTexture()->pixelFormat(); + } + const uint32_t sampleCount = targetFramebuffer->colorAttachments[attachmentIndex].sampleCount; + const uint64_t pipelineKey = clearPipelineKey(false, false, sampleCount, colorFormats, 7, depthPixelFormat); + const MTL::RenderPipelineState *pipelineState = device->findClearRenderPipelineState(pipelineKey); + if (pipelineState == nullptr) { + MTL::RenderPipelineDescriptor* pipelineDesc = MTL::RenderPipelineDescriptor::alloc()->init(); + pipelineDesc->setVertexFunction(device->clearVertexFunction); + pipelineDesc->setFragmentFunction(device->clearColorFunction); + pipelineDesc->setRasterSampleCount(sampleCount); - MTL::RenderPipelineColorAttachmentDescriptor *pipelineColorAttachment = pipelineDesc->colorAttachments()->object(attachmentIndex); - pipelineColorAttachment->setPixelFormat(targetFramebuffer->colorAttachments[attachmentIndex].getTexture()->pixelFormat()); - pipelineColorAttachment->setBlendingEnabled(false); + MTL::RenderPipelineColorAttachmentDescriptor *pipelineColorAttachment = pipelineDesc->colorAttachments()->object(attachmentIndex); + pipelineColorAttachment->setPixelFormat(targetFramebuffer->colorAttachments[attachmentIndex].getTexture()->pixelFormat()); + pipelineColorAttachment->setBlendingEnabled(false); - // Set pixel format for depth attachment if we have one, with write disabled - if (targetFramebuffer->depthAttachment.format != RenderFormat::UNKNOWN) { - pipelineDesc->setDepthAttachmentPixelFormat(targetFramebuffer->depthAttachment.getTexture()->pixelFormat()); - if (RenderFormatIsStencil(targetFramebuffer->depthAttachment.format)) { - pipelineDesc->setStencilAttachmentPixelFormat(pipelineDesc->depthAttachmentPixelFormat()); + // Set pixel format for depth attachment if we have one, with write disabled + if (hasDepth) { + pipelineDesc->setDepthAttachmentPixelFormat(depthPixelFormat); + if (RenderFormatIsStencil(targetFramebuffer->depthAttachment.format)) { + pipelineDesc->setStencilAttachmentPixelFormat(depthPixelFormat); + } } - MTL::DepthStencilDescriptor *depthStencilDescriptor = MTL::DepthStencilDescriptor::alloc()->init(); - depthStencilDescriptor->setDepthWriteEnabled(false); - const MTL::DepthStencilState *depthStencilState = device->mtl->newDepthStencilState(depthStencilDescriptor); - activeRenderEncoder->setDepthStencilState(depthStencilState); - depthStencilDescriptor->release(); + pipelineState = device->getOrCreateClearRenderPipelineState(pipelineDesc); + pipelineDesc->release(); } - const MTL::RenderPipelineState *pipelineState = device->getOrCreateClearRenderPipelineState(pipelineDesc); activeRenderEncoder->setRenderPipelineState(pipelineState); - setCommonClearState(); - pipelineDesc->release(); // Generate vertices for each rect const uint32_t rectCount = clearRectsCount > 0 ? clearRectsCount : 1; @@ -2897,23 +2940,35 @@ namespace plume { // Process clears activeRenderEncoder->pushDebugGroup(MTLSTR("DepthClear")); - MTL::RenderPipelineDescriptor* pipelineDesc = MTL::RenderPipelineDescriptor::alloc()->init(); - pipelineDesc->setVertexFunction(device->clearVertexFunction); - pipelineDesc->setFragmentFunction(device->clearDepthFunction); - pipelineDesc->setDepthAttachmentPixelFormat(targetFramebuffer->depthAttachment.getTexture()->pixelFormat()); - if (RenderFormatIsStencil(targetFramebuffer->depthAttachment.format)) { - pipelineDesc->setStencilAttachmentPixelFormat(pipelineDesc->depthAttachmentPixelFormat()); + // Look up the cached pipeline before allocating a descriptor; only a cache miss builds one. + const MTL::PixelFormat depthPixelFormat = targetFramebuffer->depthAttachment.getTexture()->pixelFormat(); + const uint32_t colorCount = static_cast(std::min(targetFramebuffer->colorAttachments.size(), 7)); + MTL::PixelFormat colorFormats[7] = {}; + for (uint32_t j = 0; j < colorCount; j++) { + colorFormats[j] = targetFramebuffer->colorAttachments[j].getTexture()->pixelFormat(); } - pipelineDesc->setRasterSampleCount(targetFramebuffer->depthAttachment.sampleCount); + const uint64_t pipelineKey = clearPipelineKey(clearDepth, clearStencil, targetFramebuffer->depthAttachment.sampleCount, colorFormats, colorCount, depthPixelFormat); + const MTL::RenderPipelineState *pipelineState = device->findClearRenderPipelineState(pipelineKey); + if (pipelineState == nullptr) { + MTL::RenderPipelineDescriptor* pipelineDesc = MTL::RenderPipelineDescriptor::alloc()->init(); + pipelineDesc->setVertexFunction(device->clearVertexFunction); + pipelineDesc->setFragmentFunction(device->clearDepthFunction); + pipelineDesc->setDepthAttachmentPixelFormat(depthPixelFormat); + if (RenderFormatIsStencil(targetFramebuffer->depthAttachment.format)) { + pipelineDesc->setStencilAttachmentPixelFormat(depthPixelFormat); + } + pipelineDesc->setRasterSampleCount(targetFramebuffer->depthAttachment.sampleCount); - // Set color attachment pixel formats with write disabled - for (uint32_t j = 0; j < targetFramebuffer->colorAttachments.size(); j++) { - MTL::RenderPipelineColorAttachmentDescriptor *pipelineColorAttachment = pipelineDesc->colorAttachments()->object(j); - pipelineColorAttachment->setPixelFormat(targetFramebuffer->colorAttachments[j].getTexture()->pixelFormat()); - pipelineColorAttachment->setWriteMask(MTL::ColorWriteMaskNone); - } + // Set color attachment pixel formats with write disabled + for (uint32_t j = 0; j < targetFramebuffer->colorAttachments.size(); j++) { + MTL::RenderPipelineColorAttachmentDescriptor *pipelineColorAttachment = pipelineDesc->colorAttachments()->object(j); + pipelineColorAttachment->setPixelFormat(targetFramebuffer->colorAttachments[j].getTexture()->pixelFormat()); + pipelineColorAttachment->setWriteMask(MTL::ColorWriteMaskNone); + } - const MTL::RenderPipelineState *pipelineState = device->getOrCreateClearRenderPipelineState(pipelineDesc, clearDepth, clearStencil); + pipelineState = device->getOrCreateClearRenderPipelineState(pipelineDesc, clearDepth, clearStencil); + pipelineDesc->release(); + } activeRenderEncoder->setRenderPipelineState(pipelineState); if (clearDepth && clearStencil) { activeRenderEncoder->setDepthStencilState(device->clearDepthStencilState); @@ -2924,7 +2979,6 @@ namespace plume { } setCommonClearState(); - pipelineDesc->release(); // Generate vertices for each rect const uint32_t rectCount = clearRectsCount > 0 ? clearRectsCount : 1; @@ -4132,6 +4186,13 @@ namespace plume { depthDescriptor->release(); stencilDescriptor->release(); + + // Partial color clears with a depth attachment: no depth or stencil writes, depth test always. + MTL::DepthStencilDescriptor *colorClearDepthDescriptor = MTL::DepthStencilDescriptor::alloc()->init(); + colorClearDepthDescriptor->setDepthWriteEnabled(false); + colorClearDepthDescriptor->setDepthCompareFunction(MTL::CompareFunctionAlways); + clearColorDepthState = mtl->newDepthStencilState(colorClearDepthDescriptor); + colorClearDepthDescriptor->release(); clearShaderLibrary->release(); } @@ -4157,6 +4218,12 @@ namespace plume { return inserted_it->second; } + MTL::RenderPipelineState* MetalDevice::findClearRenderPipelineState(uint64_t pipelineKey) { + std::lock_guard lock(clearPipelineStateMutex); + const auto it = clearRenderPipelineStates.find(pipelineKey); + return (it != clearRenderPipelineStates.end()) ? it->second : nullptr; + } + void MetalDevice::addResource(MTL::Resource *resource, bool addressable) { if (residencySet != nullptr || addressable) { std::lock_guard lock(resourcesMutex); diff --git a/plume_metal.h b/plume_metal.h index 59ae129..10bc3c4 100644 --- a/plume_metal.h +++ b/plume_metal.h @@ -711,6 +711,8 @@ namespace plume { MTL::DepthStencilState *clearDepthState; MTL::DepthStencilState *clearStencilState; MTL::DepthStencilState *clearDepthStencilState; + // Depth attached but not written (partial color clears); created once instead of per clear. + MTL::DepthStencilState *clearColorDepthState = nullptr; std::mutex clearPipelineStateMutex; std::unordered_map clearRenderPipelineStates; @@ -765,6 +767,7 @@ namespace plume { void createClearShaderLibrary(); MTL::RenderPipelineState* getOrCreateClearRenderPipelineState(MTL::RenderPipelineDescriptor *pipelineDesc, bool depthWriteEnabled = false, bool stencilWriteEnabled = false); + MTL::RenderPipelineState* findClearRenderPipelineState(uint64_t pipelineKey); void addResource(MTL::Resource *resource, bool addressable = false); void removeResource(MTL::Resource *resource, bool addressable = false);