From 6202870d75a6e5aedd0e7c7211ed0bbe5eef5bba Mon Sep 17 00:00:00 2001 From: navjack Date: Tue, 15 Sep 2026 00:25:27 -0400 Subject: [PATCH] Metal: texture readback, allocation failure reporting and batched clear rects - Copy texture regions into CPU-visible buffers so texture readback works on the Metal backend, as it does on D3D12 and Vulkan. - createTexture and createBuffer return nullptr when the Metal allocation fails instead of handing back an object wrapping a null resource, so callers can fall back. - clearDepthStencil submits rect lists longer than the per-call limit in batches, as the D3D12 backend does, instead of asserting. Co-Authored-By: Claude Opus 5 --- plume_metal.cpp | 72 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/plume_metal.cpp b/plume_metal.cpp index 373c87e..e076f92 100644 --- a/plume_metal.cpp +++ b/plume_metal.cpp @@ -1324,7 +1324,12 @@ namespace plume { } std::unique_ptr MetalPool::createTexture(const RenderTextureDesc &desc) { - return std::make_unique(device, this, desc); + // Report native allocation failure so callers can fall back. + auto texture = std::make_unique(device, this, desc); + if (texture->mtl == nullptr) { + return nullptr; + } + return texture; } // MetalShader @@ -2864,6 +2869,15 @@ namespace plume { } void MetalCommandList::clearDepthStencil(const bool clearDepth, const bool clearStencil, const float depthValue, const uint32_t stencilValue, const RenderRect *clearRects, const uint32_t clearRectsCount) { + // Submit large rect lists in batches, as the D3D12 backend does. + if (clearRects != nullptr && clearRectsCount > MAX_CLEAR_RECTS) { + for (uint32_t first = 0; first < clearRectsCount; first += MAX_CLEAR_RECTS) { + const uint32_t count = std::min(clearRectsCount - first, MAX_CLEAR_RECTS); + clearDepthStencil(clearDepth, clearStencil, depthValue, stencilValue, clearRects + first, count); + } + return; + } + assert(targetFramebuffer != nullptr); assert(targetFramebuffer->depthAttachment.format != RenderFormat::UNKNOWN); assert((!clearRects || clearRectsCount <= MAX_CLEAR_RECTS) && "Too many clear rects"); @@ -3022,6 +3036,48 @@ namespace plume { dstOrigin ); activeBlitEncoder->popDebugGroup(); + } else if (dstLocation.type == RenderTextureCopyType::PLACED_FOOTPRINT && srcLocation.type == RenderTextureCopyType::SUBRESOURCE) { + // Texture readback into a CPU-visible buffer. + assert(dstBuffer != nullptr); + assert(srcTexture != nullptr); + + const RenderFormat format = dstLocation.placedFootprint.format; + const uint32_t blockWidth = RenderFormatBlockWidth(format); + MTL::Origin srcOrigin = { 0, 0, 0 }; + MTL::Size size = { dstLocation.placedFootprint.width, dstLocation.placedFootprint.height, dstLocation.placedFootprint.depth }; + if (srcBox != nullptr) { + srcOrigin = { NS::UInteger(srcBox->left), NS::UInteger(srcBox->top), NS::UInteger(srcBox->front) }; + size = { NS::UInteger(srcBox->right - srcBox->left), NS::UInteger(srcBox->bottom - srcBox->top), NS::UInteger(srcBox->back - srcBox->front) }; + } + + // Metal rejects copies that extend past the source mip level. + const NS::UInteger levelWidth = std::max(srcTexture->desc.width >> srcLocation.subresource.mipLevel, 1); + const NS::UInteger levelHeight = std::max(srcTexture->desc.height >> srcLocation.subresource.mipLevel, 1); + size.width = std::min(size.width, levelWidth - srcOrigin.x); + size.height = std::min(size.height, levelHeight - srcOrigin.y); + size.depth = std::max(size.depth, 1); + + const uint32_t horizontalBlocks = (dstLocation.placedFootprint.rowWidth + blockWidth - 1) / blockWidth; + const uint32_t verticalBlocks = (uint32_t(size.height) + blockWidth - 1) / blockWidth; + const NS::UInteger bytesPerRow = NS::UInteger(horizontalBlocks) * RenderFormatSize(format); + const NS::UInteger bytesPerImage = bytesPerRow * verticalBlocks; + // Combined depth-stencil textures need an explicit aspect to read depth. + const MTL::BlitOption options = RenderFormatIsStencil(srcTexture->desc.format) ? MTL::BlitOptionDepthFromDepthStencil : MTL::BlitOptionNone; + + activeBlitEncoder->pushDebugGroup(MTLSTR("ReadbackTextureRegion")); + activeBlitEncoder->copyFromTexture( + srcTexture->mtl, + srcLocation.subresource.arrayIndex, + srcLocation.subresource.mipLevel, + srcOrigin, + size, + dstBuffer->mtl, + dstLocation.placedFootprint.offset, + bytesPerRow, + bytesPerImage, + options + ); + activeBlitEncoder->popDebugGroup(); } else { assert(dstTexture != nullptr); assert(srcTexture != nullptr); @@ -3910,11 +3966,21 @@ namespace plume { } std::unique_ptr MetalDevice::createBuffer(const RenderBufferDesc &desc) { - return std::make_unique(this, nullptr, desc); + // Report native allocation failure so callers can fall back. + auto buffer = std::make_unique(this, nullptr, desc); + if (buffer->mtl == nullptr && desc.size > 0) { + return nullptr; + } + return buffer; } std::unique_ptr MetalDevice::createTexture(const RenderTextureDesc &desc) { - return std::make_unique(this, nullptr, desc); + // Report native allocation failure so callers can fall back. + auto texture = std::make_unique(this, nullptr, desc); + if (texture->mtl == nullptr) { + return nullptr; + } + return texture; } std::unique_ptr MetalDevice::createAccelerationStructure(const RenderAccelerationStructureDesc &desc) {