From b1f9ea39f19e70bd981ef8511b1c0e7d7dc48c59 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 11:25:25 -0400 Subject: [PATCH] [MG_Backend, MG_IntegrationTest] (P7): copy 1:1 single-sample depth/stencil blits on the Magma wire arm --- .../DirectVulkan/Renderer/VulkanRenderer.h | 13 + .../DirectVulkan/Renderer/WireFramebuffer.inc | 230 +++++++++++++----- MobileGL/MG_IntegrationTest/CMakeLists.txt | 15 ++ .../DepthStencilReadbackMatrixScenario.cpp | 175 +++++++++++++ 4 files changed, 376 insertions(+), 57 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index a3093d50..e6a8c601 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -517,6 +517,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ResolveWireDepthStencil(WireImage source, WireImage destination, GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1); + // One aspect of a depth/stencil image through a buffer (source in TRANSFER_SRC_OPTIMAL, + // destination in TRANSFER_DST_OPTIMAL, one format). The returned buffer is named by the + // recorded commands and must be retired with them. + UniquePtr CopyWireAspectThroughBuffer(VkImage srcImage, Uint32 srcLevel, Uint32 srcLayer, + GLint sx, GLint sy, const WireImage& destination, + GLint dx, GLint dy, Uint32 width, Uint32 height, + VkImageAspectFlags aspect, VkFormat format, + Bool mirrorY, const char* fatalDetail); + // A single-sample depth/stencil blit whose rectangles make it a plain copy, performed + // without vkCmdBlitImage (P7 gate 5). False when the shape is not a plain copy. + Bool CopyWireDepthStencilBlit(WireImage& source, WireImage& destination, + GLint sx0, GLint sy0, GLint sx1, GLint sy1, + GLint dx0, GLint dy0, GLint dx1, GLint dy1, VkImageAspectFlags aspect); // True when the rectangles give a multisample depth/stencil resolve a shape it declines // (a scale, an X mirror, a Y mirror across two formats); the decline has then been // logged and its INVALID_OPERATION recorded. BlitWireFramebuffers asks it BEFORE any diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index ebdc7645..0d141a32 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -116,6 +116,11 @@ return forced; } + static Bool WireFormatIsPackedDepthStencil(VkFormat format) { + return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || + format == VK_FORMAT_D32_SFLOAT_S8_UINT; + } + VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage( const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, Bool isWriteTarget) { @@ -494,6 +499,136 @@ return false; } + // ONE ASPECT OF A DEPTH/STENCIL IMAGE, MOVED THROUGH A BUFFER. The source image must be in + // TRANSFER_SRC_OPTIMAL and the destination in TRANSFER_DST_OPTIMAL, both of `format`. A + // buffer copy names exactly one aspect and moves exactly its texels, which an image copy of a + // packed depth-stencil format does not promise (some drivers move the whole native word, so + // a depth-only region would replace the destination's stencil). The multisample resolve's + // copy-out and the single-sample copy arm share this body; the returned buffer is still named + // by the recorded commands and must be retired with them. + UniquePtr VulkanRenderer::CopyWireAspectThroughBuffer( + VkImage srcImage, Uint32 srcLevel, Uint32 srcLayer, GLint sx, GLint sy, + const WireImage& destination, GLint dx, GLint dy, Uint32 width, Uint32 height, + VkImageAspectFlags aspect, VkFormat format, Bool mirrorY, const char* fatalDetail) { + auto& frame = m_frameContext.GetCurrent(); + const VkDeviceSize texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : + (format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT ? 2 : 4); + // THE ROW STRIDE IS PADDED TO 4 BYTES. A depth/stencil buffer<->image copy wants + // every bufferOffset on a multiple of 4 (VUID-vkCmdCopyBufferToImage-dstImage-07978, + // pRegions-07978 in older layers), and the mirror below starts a region on every + // row - so a tight row of 1-byte stencil texels whose width is not a multiple of 4, + // or of 2-byte D16 texels with an odd width, put every row after the first on an + // illegal offset. The scissor clip in BlitWireFramebuffers produces exactly such + // widths. Both copies name the padded length as bufferRowLength, so the buffer + // layout is one layout whichever way it is walked. + const VkDeviceSize texelsPer4Bytes = 4 / texelSize; + const VkDeviceSize paddedWidth = + (static_cast(width) + texelsPer4Bytes - 1) / texelsPer4Bytes * texelsPer4Bytes; + const VkDeviceSize bytes = paddedWidth * height * texelSize; + auto aspectBuffer = MakeUnique(); + if (!aspectBuffer->Create({.allocator = m_allocator, .size = bytes, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE})) + MagmaWireFatal(fatalDetail); + VkBufferImageCopy copy{}; + copy.bufferRowLength = static_cast(paddedWidth); + copy.imageSubresource = {aspect, srcLevel, srcLayer, 1}; + copy.imageOffset = {sx, sy, 0}; + copy.imageExtent = {width, height, 1}; + vkCmdCopyImageToBuffer(frame.commandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + aspectBuffer->GetHandle(), 1, ©); + VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.buffer = aspectBuffer->GetHandle(); + barrier.size = bytes; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 1, &barrier, 0, nullptr); + copy.imageSubresource = {aspect, destination.level, destination.layer, 1}; + copy.imageOffset = {dx, dy, 0}; + if (!mirrorY) { + vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } else { + // THE VERTICAL MIRROR, and it costs one region per row rather than a pass. The + // buffer holds the band bottom row first (the image copy above wrote it from sy + // upwards, one padded stride per row); writing row r of that buffer to destination + // row dy + height - 1 - r reverses the band. Each region starts on a padded row, + // which is what keeps its bufferOffset a multiple of 4. + Vector rows(static_cast(height), copy); + const VkDeviceSize rowBytes = paddedWidth * texelSize; + for (Uint32 row = 0; row < height; ++row) { + auto& region = rows[static_cast(row)]; + region.bufferOffset = static_cast(row) * rowBytes; + region.imageOffset = {dx, static_cast(static_cast(dy) + height - 1 - row), 0}; + region.imageExtent = {width, 1, 1}; + } + vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(rows.size()), rows.data()); + } + return aspectBuffer; + } + + // P7 gate 5 (g5-msrbo): A SINGLE-SAMPLE DEPTH/STENCIL BLIT THAT IS A PLAIN COPY IS A COPY. + // Same size, same orientation on both axes, same native format, two distinct non-default, + // non-3D images, and the rectangle inside both: nothing a blit adds (scale, filter, flip, + // format conversion) is asked for, so the aspect moves with vkCmdCopyImage - or, for one + // aspect of a packed depth-stencil format, through CopyWireAspectThroughBuffer so the + // destination's other aspect survives. That is what the monolith arm does + // (VulkanRenderer::BlitNamedFramebuffer: vkCmdCopyImage at 1:1, CopyDepthStencilAspectThrough- + // Buffer for one aspect of a packed format), and neither command needs BLIT_SRC/BLIT_DST, + // which a depth/stencil format does not have to carry (the Adreno 830 reports BLIT_SRC and no + // BLIT_DST for every depth format). The wire arm sent every depth/stencil blit to + // vkCmdBlitImage, and for one aspect of a packed format that moved the whole native word: + // lavapipe replaced the destination's stencil on a depth-only blit. Returns false when the + // shape is not a plain copy; the caller then takes the vkCmdBlitImage arm as before. + Bool VulkanRenderer::CopyWireDepthStencilBlit(WireImage& source, WireImage& destination, + GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1, + VkImageAspectFlags aspect) { + const Int64 spanX = static_cast(sx1) - sx0; + const Int64 spanY = static_cast(sy1) - sy0; + if (spanX == 0 || spanY == 0 || spanX != static_cast(dx1) - dx0 || + spanY != static_cast(dy1) - dy0) + return false; + if (source.isDefault || destination.isDefault || source.is3D || destination.is3D || + source.image == destination.image || source.format != destination.format) + return false; + const Int64 width = spanX < 0 ? -spanX : spanX; + const Int64 height = spanY < 0 ? -spanY : spanY; + const Int64 sxMin = std::min(sx0, sx1), syMin = std::min(sy0, sy1); + const Int64 dxMin = std::min(dx0, dx1), dyMin = std::min(dy0, dy1); + if (sxMin < 0 || syMin < 0 || dxMin < 0 || dyMin < 0 || + sxMin + width > static_cast(source.extent.width) || + syMin + height > static_cast(source.extent.height) || + dxMin + width > static_cast(destination.extent.width) || + dyMin + height > static_cast(destination.extent.height)) + return false; + TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + TransitionWireImage(destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + if (WireFormatIsPackedDepthStencil(source.format)) { + WireRetiredObjects retired{}; + retired.buffers.push_back(CopyWireAspectThroughBuffer( + source.image, source.level, source.layer, static_cast(sxMin), + static_cast(syMin), destination, static_cast(dxMin), + static_cast(dyMin), static_cast(width), static_cast(height), + aspect, source.format, /*mirrorY=*/false, "depth-stencil-copy-buffer")); + RetireWireObjects(Move(retired)); + return true; + } + VkImageCopy copy{}; + copy.srcSubresource = {aspect, source.level, source.layer, 1}; + copy.srcOffset = {static_cast(sxMin), static_cast(syMin), 0}; + copy.dstSubresource = {aspect, destination.level, destination.layer, 1}; + copy.dstOffset = {static_cast(dxMin), static_cast(dyMin), 0}; + copy.extent = {static_cast(width), static_cast(height), 1}; + vkCmdCopyImage(m_frameContext.GetCurrent().commandBuffer, source.image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + return true; + } + void VulkanRenderer::ResolveWireDepthStencil(WireImage source, WireImage destination, GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1) { // P7 wave 2-B2 (review round): A REVERSED RECTANGLE IS NOT A MALFORMED ONE. This guard @@ -690,63 +825,11 @@ // depth/stencil image-copy paths copy the complete native word even // for a single-aspect region. A stencil byte or depth-only word in // this GPU buffer cannot replace the destination's other aspect. - const VkDeviceSize texelSize = source.aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : - (source.format == VK_FORMAT_D16_UNORM || source.format == VK_FORMAT_D16_UNORM_S8_UINT ? 2 : 4); - // THE ROW STRIDE IS PADDED TO 4 BYTES. A depth/stencil buffer<->image copy wants - // every bufferOffset on a multiple of 4 (VUID-vkCmdCopyBufferToImage-dstImage-07978, - // pRegions-07978 in older layers), and the mirror below starts a region on every - // row - so a tight row of 1-byte stencil texels whose width is not a multiple of 4, - // or of 2-byte D16 texels with an odd width, put every row after the first on an - // illegal offset. The scissor clip in BlitWireFramebuffers produces exactly such - // widths. Both copies name the padded length as bufferRowLength, so the buffer - // layout is one layout whichever way it is walked. - const VkDeviceSize texelsPer4Bytes = 4 / texelSize; - const VkDeviceSize paddedWidth = - (static_cast(width) + texelsPer4Bytes - 1) / texelsPer4Bytes * texelsPer4Bytes; - const VkDeviceSize bytes = paddedWidth * height * texelSize; - aspectBuffer = MakeUnique(); - if (!aspectBuffer->Create({.allocator = m_allocator, .size = bytes, - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE})) - MagmaWireFatal("multisample-depth-resolve-buffer"); - VkBufferImageCopy copy{}; - copy.bufferRowLength = static_cast(paddedWidth); - copy.imageSubresource = {source.aspect, 0, 0, 1}; - copy.imageOffset = {sxMin, syMin, 0}; - copy.imageExtent = {static_cast(width), static_cast(height), 1}; - vkCmdCopyImageToBuffer(frame.commandBuffer, resolved.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - aspectBuffer->GetHandle(), 1, ©); - VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.buffer = aspectBuffer->GetHandle(); - barrier.size = bytes; - vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 1, &barrier, 0, nullptr); - copy.imageSubresource = {source.aspect, destination.level, destination.layer, 1}; - copy.imageOffset = {dxMin, dyMin, 0}; - if (!mirrorY) { - vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); - } else { - // THE VERTICAL MIRROR, and it costs one region per row rather than a pass. The - // buffer holds the resolved band bottom row first (the image copy above wrote it - // from syMin upwards, one padded stride per row); writing row r of that buffer - // to destination row dyMin + height - 1 - r reverses the band. Each region - // starts on a padded row, which is what keeps its bufferOffset a multiple of 4. - Vector rows(static_cast(height), copy); - const VkDeviceSize rowBytes = paddedWidth * texelSize; - for (Int64 row = 0; row < height; ++row) { - auto& region = rows[static_cast(row)]; - region.bufferOffset = static_cast(row) * rowBytes; - region.imageOffset = {dxMin, static_cast(dyMin + height - 1 - row), 0}; - region.imageExtent = {static_cast(width), 1, 1}; - } - vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - static_cast(rows.size()), rows.data()); - } + aspectBuffer = CopyWireAspectThroughBuffer(resolved.image, 0, 0, sxMin, syMin, destination, + dxMin, dyMin, static_cast(width), + static_cast(height), source.aspect, + source.format, mirrorY, + "multisample-depth-resolve-buffer"); } else { // A logical GL depth format may use different native renderbuffer // and texture formats. Re-encode only after producing real resolved @@ -962,6 +1045,39 @@ RetireWireObjects(Move(retired)); return; } + // P7 gate 5 (g5-msrbo): A SINGLE-SAMPLE DEPTH/STENCIL BLIT THAT IS A PLAIN COPY IS A COPY + // (CopyWireDepthStencilBlit): no blit feature, and one aspect of a packed format moves + // without touching the other. Anything else - a scale, a flip, the default framebuffer, + // a format change - still takes vkCmdBlitImage below, exactly as it did and as the + // monolith arm does. On a format without BLIT_SRC/BLIT_DST that command is outside the + // spec, and the Adreno 830 (which has no BLIT_DST on any depth format) executes it + // anyway: declining would have turned a working picture on the target device into a + // missing one. So it is named once, for the next device reading, and not refused. + if (aspect != VK_IMAGE_ASPECT_COLOR_BIT && source.samples == VK_SAMPLE_COUNT_1_BIT && + destination.samples == VK_SAMPLE_COUNT_1_BIT) { + if (CopyWireDepthStencilBlit(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1, aspect)) { + if (dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, + destination.layer, 1); + return; + } + VkFormatProperties sourceProperties{}; + VkFormatProperties destinationProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties); + vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, + &destinationProperties); + if (!(sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) || + !(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) { + MGLOG_W_ONCE("DirectVulkan::BlitWireFramebuffers: aspect 0x%x from format %d (features 0x%x) " + "to format %d (features 0x%x) is not a plain copy and takes vkCmdBlitImage " + "without BLIT_SRC/BLIT_DST - the monolith arm's command, outside the spec; the " + "driver decides the picture", + static_cast(aspect), static_cast(source.format), + static_cast(sourceProperties.optimalTilingFeatures), + static_cast(destination.format), + static_cast(destinationProperties.optimalTilingFeatures)); + } + } // GL names logical window coordinates. Android's native swapchain may // be quarter-turned (e.g. logical 2620x1280 over an image 1280x2620). // An axis-aligned vkCmdBlitImage cannot express that rotation; issuing diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 04a4e90c..98524cd2 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3364,6 +3364,21 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=${shaderDepthResolve}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msresolve${mglItestArmLogTag}-shader${shaderDepthResolve}.log") endforeach() + # ---- P7 gate 5 (g5-msrbo): the SINGLE-SAMPLED depth/stencil blit ---------------------- + # + # KHR-GL46.direct_state_access.renderbuffers_storage_multisample's samples == 0 leg, every + # depth/stencil format: the wire arm moved every single-sample depth/stencil aspect with + # vkCmdBlitImage where the monolith copies at 1:1, and for one aspect of a packed format + # that moved the whole native word (a depth-only blit replaced the destination's stencil + # on lavapipe). NO KNOB: the packed-aspect leg is red on every host without the copy arm, + # so all three arms carry it. + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.DsBlit." + TEST_FILTER "DepthStencilReadbackMatrixScenario.ASingleSampledDepthStencilBlitCopiesEveryAspect" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-dsblit${mglItestArmLogTag}.log") # P7 wave 2-B2 (review round): a FLIPPED multisample depth/stencil resolve is a legal blit # that used to end the session, and a SCALED one is INVALID_OPERATION that used to end it # too. Both shapes are decided on the rectangles, before any capability question, so this diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp index d1d5298f..32988588 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp @@ -682,6 +682,181 @@ namespace MGITest { Gl().EndFrame(); } + // P7 gate 5 (g5-msrbo): A SINGLE-SAMPLED DEPTH/STENCIL BLIT, EVERY FORMAT, EVERY ASPECT. + // + // KHR-GL46.direct_state_access.renderbuffers_storage_multisample reduced to its samples == 0 + // leg: two renderbuffers of one depth/stencil format (glNamedRenderbufferStorageMultisample + // with samples 0, the conformance case's own call), clear the first, glBlitFramebuffer + // COLOR|DEPTH|STENCIL into the second at 1:1, read both aspects back from the second. The + // wire arm moved every depth/stencil aspect with vkCmdBlitImage, which needs BLIT_SRC/BLIT_DST + // (optional for depth/stencil formats: the Adreno 830 has no BLIT_DST on any of them) where + // the monolith arm copies at 1:1 - and for ONE aspect of a packed format the blit moved the + // whole native word. + // + // THE SUB-RECTANGLE LEG is the red one without the copy arm, on any host: a depth-only blit + // of a packed DEPTH24_STENCIL8 replaced the destination's stencil on lavapipe. It also pins + // the min corners on both sides and a 1-byte stencil row whose width is not a multiple of 4 + // (the buffer round trip's padded stride). THE SCALED LEG is the shape that still takes + // vkCmdBlitImage, so the copy arm must not have swallowed it. + TEST_F(DepthStencilReadbackMatrixScenario, ASingleSampledDepthStencilBlitCopiesEveryAspect) { + if (!Ready()) return; + struct BlitFormat { + const char* name; + GLenum internalFormat; + }; + const BlitFormat formats[] = { + {"GL_DEPTH_COMPONENT16", GL_DEPTH_COMPONENT16}, {"GL_DEPTH_COMPONENT24", GL_DEPTH_COMPONENT24}, + {"GL_DEPTH_COMPONENT32F", GL_DEPTH_COMPONENT32F}, {"GL_DEPTH24_STENCIL8", GL_DEPTH24_STENCIL8}, + {"GL_DEPTH32F_STENCIL8", GL_DEPTH32F_STENCIL8}, {"GL_STENCIL_INDEX8", GL_STENCIL_INDEX8}, + }; + struct Extent { + int width, height; + }; + // The conformance case's three shapes ({1,1}, {max/2,1}, {1,max/2}), with strips short + // enough for a CPU rasterizer, plus the matrix's own rectangle. + const Extent extents[] = {{1, 1}, {256, 1}, {1, 256}, {kWidth, kHeight}}; + + const auto makePair = [](GLenum internalFormat, int width, int height, GLuint (&fbo)[2], GLuint (&rbo)[2]) { + glGenFramebuffers(2, fbo); + glCreateRenderbuffers(2, rbo); + for (int i = 0; i < 2; ++i) { + glNamedRenderbufferStorageMultisample(rbo[i], 0, internalFormat, width, height); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[i]); + if (FormatHasDepth(internalFormat)) + glNamedFramebufferRenderbuffer(fbo[i], GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo[i]); + if (FormatHasStencil(internalFormat)) + glNamedFramebufferRenderbuffer(fbo[i], GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo[i]); + } + }; + const auto clearBound = [](GLenum internalFormat, int width, int height, float depth, int stencil) { + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, width, height); + glDepthMask(GL_TRUE); + glStencilMask(0xFFu); + glClearDepth(depth); + glClearStencil(stencil); + GLbitfield mask = 0; + if (FormatHasDepth(internalFormat)) mask |= GL_DEPTH_BUFFER_BIT; + if (FormatHasStencil(internalFormat)) mask |= GL_STENCIL_BUFFER_BIT; + glClear(mask); + }; + + int exercised = 0; + for (const BlitFormat& format : formats) { + for (const Extent& extent : extents) { + SCOPED_TRACE(std::string(format.name) + " " + std::to_string(extent.width) + "x" + + std::to_string(extent.height)); + GLuint fbo[2] = {0, 0}; + GLuint rbo[2] = {0, 0}; + makePair(format.internalFormat, extent.width, extent.height, fbo, rbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + const bool usable = SourceIsUsable(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + if (!usable || !SourceIsUsable()) { + glDeleteFramebuffers(2, fbo); + glDeleteRenderbuffers(2, rbo); + FirstGLError(); + continue; + } + ASSERT_EQ(FirstGLError(), 0u) << "creating the renderbuffer pair"; + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + clearBound(format.internalFormat, extent.width, extent.height, kDepthPoison, kStencilPoison); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + clearBound(format.internalFormat, extent.width, extent.height, 0.5f, 7); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]); + glBlitFramebuffer(0, 0, extent.width, extent.height, 0, 0, extent.width, extent.height, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST); + EXPECT_EQ(FirstGLError(), 0u) << "a 1:1 single-sampled depth/stencil blit"; + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + if (FormatHasDepth(format.internalFormat)) + ExpectAllDepth(ReadDepthFloat(0, 0, extent.width, extent.height), 0.5f, "blitted depth"); + if (FormatHasStencil(format.internalFormat)) + ExpectAllStencil(ReadStencilInt(0, 0, extent.width, extent.height), 7, "blitted stencil"); + EXPECT_EQ(FirstGLError(), 0u); + ++exercised; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(2, fbo); + glDeleteRenderbuffers(2, rbo); + } + } + // Six formats times four extents; a machine that hosts fewer than half is not a matrix. + EXPECT_GE(exercised, 12) << "too few depth/stencil renderbuffer pairs were usable"; + + // THE SUB-RECTANGLE LEG, packed DEPTH24_STENCIL8: a depth-only band, then a stencil-only + // band, each offset from the origin on both sides and narrow enough that a 1-byte stencil + // row is not a multiple of 4. + { + GLuint fbo[2] = {0, 0}; + GLuint rbo[2] = {0, 0}; + makePair(GL_DEPTH24_STENCIL8, kWidth, kHeight, fbo, rbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + ASSERT_TRUE(SourceIsUsable()); + clearBound(GL_DEPTH24_STENCIL8, kWidth, kHeight, 0.75f, 99); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + clearBound(GL_DEPTH24_STENCIL8, kWidth, kHeight, 0.25f, 11); + ASSERT_EQ(FirstGLError(), 0u); + constexpr int kSx = 5, kSy = 8, kDx = 20, kDy = 4, kW = 29, kH = 32; + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]); + glBlitFramebuffer(kSx, kSy, kSx + kW, kSy + kH, kDx, kDy, kDx + kW, kDy + kH, GL_DEPTH_BUFFER_BIT, + GL_NEAREST); + EXPECT_EQ(FirstGLError(), 0u) << "a depth-only sub-rectangle blit"; + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + const std::vector depth = ReadDepthFloat(0, 0, kWidth, kHeight); + const std::vector stencil = ReadStencilInt(0, 0, kWidth, kHeight); + size_t badDepth = 0, badStencil = 0; + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + const bool inside = x >= kDx && x < kDx + kW && y >= kDy && y < kDy + kH; + const size_t at = static_cast(y) * kWidth + x; + if (std::fabs(depth[at] - (inside ? 0.75f : 0.25f)) > 1.0f / 4096.0f) ++badDepth; + if (stencil[at] != 11) ++badStencil; + } + } + EXPECT_EQ(badDepth, 0u) << "the depth-only band landed somewhere other than its destination rectangle"; + EXPECT_EQ(badStencil, 0u) << "a depth-only blit replaced the destination's stencil"; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]); + glBlitFramebuffer(kSx, kSy, kSx + kW, kSy + kH, kDx, kDy, kDx + kW, kDy + kH, GL_STENCIL_BUFFER_BIT, + GL_NEAREST); + EXPECT_EQ(FirstGLError(), 0u) << "a stencil-only sub-rectangle blit"; + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + const std::vector depthAfter = ReadDepthFloat(0, 0, kWidth, kHeight); + const std::vector stencilAfter = ReadStencilInt(0, 0, kWidth, kHeight); + badDepth = badStencil = 0; + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + const bool inside = x >= kDx && x < kDx + kW && y >= kDy && y < kDy + kH; + const size_t at = static_cast(y) * kWidth + x; + if (std::fabs(depthAfter[at] - (inside ? 0.75f : 0.25f)) > 1.0f / 4096.0f) ++badDepth; + if (stencilAfter[at] != (inside ? 99 : 11)) ++badStencil; + } + } + EXPECT_EQ(badStencil, 0u) << "the stencil-only band landed somewhere other than its destination rectangle"; + EXPECT_EQ(badDepth, 0u) << "a stencil-only blit replaced the destination's depth"; + + // THE SCALED LEG: half the source onto the whole destination - the shape that still + // takes vkCmdBlitImage. + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + clearBound(GL_DEPTH24_STENCIL8, kWidth, kHeight, 0.25f, 11); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]); + glBlitFramebuffer(0, 0, kWidth / 2, kHeight / 2, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + glFinish(); + EXPECT_EQ(FirstGLError(), 0u) << "a scaled depth blit with NEAREST is legal GL"; + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + ExpectAllDepth(ReadDepthFloat(0, 0, kWidth, kHeight), 0.75f, "a scaled depth blit"); + EXPECT_EQ(FirstGLError(), 0u) << "the session survived the scaled leg"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(2, fbo); + glDeleteRenderbuffers(2, rbo); + } + Gl().EndFrame(); + } + // A read whose rectangle is NOT the whole attachment. The staging copy has to carry // the requested rect (not the origin) and hand back its rows bottom-up, which a // full-extent uniform read is a fixed point of and therefore cannot see.