diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp index 2f507cbb..e9191dc0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp @@ -8,6 +8,10 @@ #include "VkClearManager.h" +// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the +// clear key's layer span has to be the same one the render pass builds its attachment view from. +#include "VkTextureManager.h" + #include "MG_State/GLState/Core.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" @@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { return ResolveAttachmentBaseArrayLayer(uploadTarget); } - static Uint32 ResolveAttachmentLayerCount( - const MG_State::GLState::FramebufferAttachmentObject& attachment) { - if (attachment.IsLayered()) { - return static_cast(std::max(attachment.GetSize().z(), 1)); - } - return 1u; - } + // ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z() + // raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is + // not a cosmetic difference: the count below is not key-only, it is written straight into + // VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS + // the entry - so a layered cube map's glClear reached one face and the other five were lost + // for good, while the very same queued clear cleared all six through the render pass's + // LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent. static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment( const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index dc9c78dc..ce588ccf 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -86,29 +86,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { return ToStorageArrayLayer(texture, face); } - // The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the - // state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this - // remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a - // geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's - // upper layers were never written at all. - static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) { - if (attachment.IsLayered()) { - const auto& texture = attachment.GetTexture(); - const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown; - // A layered CUBE MAP names all six faces (GL 4.6 core 9.2.8), but the attachment model - // records only the REPRESENTATIVE upload target for it - the +X face - // (ResolveRepresentableFramebufferTextureUploadTarget) - and that face's level size has - // z = 1. Reading z here therefore attached one face to a layered framebuffer, so a - // geometry shader writing gl_Layer = 1..5 lost five sixths of its output. The image's - // six layers are the cube's faces, exactly as for a cube ARRAY (whose representative - // target does carry 6n in z and needs no special case). - if (target == TextureTarget::TextureCubeMap) { - return 6u; - } - return static_cast(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1)); - } - return 1u; - } + // ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because + // VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real + // VkImageSubresourceRange when a clear is materialised outside a render pass. See the header. // VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have // been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view @@ -1503,13 +1483,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { renderPassCreateInfo.dependencyCount = 2; renderPassCreateInfo.pDependencies = subpassDependencies; + // NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the + // INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a + // DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through + // VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here + // is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY + // would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro, + // before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal + // line on every draw for the life of the process - a decline caches nothing, so every + // later draw to the same framebuffer re-enters this path and fails again. VkRenderPass renderPass = VK_NULL_HANDLE; - VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass)); - // VK_VERIFY only logs (and only in an INFO build); the handle is the truth. Caching an - // entry whose VkRenderPass is null would hand VK_NULL_HANDLE to vkCmdBeginRenderPass and - // to every pipeline built against it. - if (renderPass == VK_NULL_HANDLE) { - MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed for FBO %u; declining the render pass", + const VkResult renderPassResult = + vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass); + if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) { + MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the " + "render pass", + VkResultToString(renderPassResult), static_cast(renderPassResult), fbo.GetExternalIndex()); return nullptr; } @@ -1525,13 +1514,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { framebufferCreateInfo.width = width; framebufferCreateInfo.height = height; framebufferCreateInfo.layers = framebufferLayers; + // Direct VkResult check, for the same reason as vkCreateRenderPass above. VkFramebuffer framebuffer = VK_NULL_HANDLE; - VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer)); - if (framebuffer == VK_NULL_HANDLE) { + const VkResult framebufferResult = + vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer); + if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) { // The render pass has no entry to own it yet, so it is destroyed here rather than // leaked - RenderPassEntry's destructor is the only other thing that would. - MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed for FBO %u (%dx%d, %u attachments, " - "%u layers); declining the render pass", + MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, " + "%u attachments, %u layers); declining the render pass", + VkResultToString(framebufferResult), static_cast(framebufferResult), fbo.GetExternalIndex(), width, height, static_cast(attachmentViews.size()), framebufferLayers); vkDestroyRenderPass(m_device, renderPass, nullptr); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 8379989e..434b39bd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -10,8 +10,10 @@ #include "../VkIncludes.h" #include +#include #include #include +#include #include #include @@ -41,6 +43,37 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT return glTexelSize; } +// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans. +// +// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and +// again in VkClearManager.cpp - and the two are not independent: the render pass builds the +// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the +// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is +// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers +// of the same GL clear, so any disagreement means the same glClear produces two different pictures +// depending only on which path happens to consume it first - and the materialise path then POPS +// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how +// that split gets introduced; keep them the same function. +// +// Two shapes make this more than `size.z()`: +// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent +// just above), so z reads 1 and every layer above the first was silently dropped. +// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face +// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 - +// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's +// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z. +inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) { + if (!attachment.IsLayered()) { + return 1u; + } + const auto& texture = attachment.GetTexture(); + const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown; + if (target == TextureTarget::TextureCubeMap) { + return 6u; + } + return static_cast(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1)); +} + // A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture // the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they // are relative to the VIEW, and have to be shifted into the storage image's numbering before they diff --git a/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp index c309a82c..c24d6070 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp @@ -32,9 +32,10 @@ // too, so the per-slice branch that exists for exactly this case was unreachable and every // slice above z = 0 came back VK_NULL_HANDLE. // -// The five cases below are those shapes - layered 3D, one 3D slice, layered cube-map array, and -// its depth and packed depth-stencil attachments - and each one asserts LAYER ROUTING, not merely -// survival: the colour a layer receives is a function of its own index, so an attachment that +// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with +// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D +// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING, +// not merely survival: what a layer receives is a function of its own index, so an attachment that // collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach // rather than passing quietly. Every texture is seeded with a poison value first, so "the draw // never landed here" reads differently from "the wrong layer landed here". @@ -52,6 +53,7 @@ // red on DirectVulkan alone means Magma is. #include +#include #include #include @@ -81,10 +83,19 @@ namespace MGITest { // right whether or not the slice is resolved at all. constexpr int kSubjectSlice = 2; + // Layers of the 1D array whose clear the last case checks. Its layer count lives in the + // state-side HEIGHT, not in z, which is the whole reason it is here. + constexpr int kOneDArrayLayers = 4; + // A colour no pass paints, uploaded before every draw. A layer that reads it back was // never rendered to. constexpr GLubyte kPoison = 0xAB; + // The glClear colour the two materialise cases use. Chosen as exact 8-bit values and fed + // to glClearColor as n/255, so the round trip through a UNORM8 target is lossless and a + // mismatch means a real miss rather than rounding. + constexpr Rgba8 kClearColor{17, 68, 187, 255}; + // What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write // says which layer it came from) and b names the PASS (so "the second draw was not // rejected" is distinguishable from "the first draw never happened"). @@ -148,6 +159,21 @@ void main() float(3 + u_pass * 60) / 255.0, 1.0); } +)"; + + // The two clear cases do not draw into the layered attachment at all - they SAMPLE it, so + // the queued clear is consumed by MaterializePendingClearForTexture rather than by a render + // pass's LOAD_OP_CLEAR. What the sample returns is irrelevant; being sampled is the point. + const char* const kCubeSampleFragmentSource = R"(#version 420 core +uniform samplerCube u_source; +out vec4 o_color; +void main() { o_color = texture(u_source, vec3(1.0, 0.0, 0.0)); } +)"; + + const char* const kOneDArraySampleFragmentSource = R"(#version 420 core +uniform sampler1DArray u_source; +out vec4 o_color; +void main() { o_color = texture(u_source, vec2(0.5, 0.0)); } )"; // The non-layered case has no geometry stage at all - the slice comes from the @@ -312,6 +338,103 @@ void main() return texture; } + // A plain RGBA8 CUBE MAP (not an array), every face poisoned. This is the shape whose + // layered attachment records the +X face as its representative upload target, so its + // level size reads z = 1 - the reason a shared layer-count helper is needed at all. + GLuint MakePoisonedCubeMap() { + const GLuint texture = TrackTexture(); + glBindTexture(GL_TEXTURE_CUBE_MAP, texture); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent); + const std::vector seed(static_cast(kExtent) * kExtent * 4, kPoison); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + for (int face = 0; face < 6; ++face) { + glTexSubImage2D(static_cast(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent, + kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data()); + } + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + return texture; + } + + // An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count - + // that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z + // gives 1 however many layers there are. + GLuint MakePoisoned1DArray() { + const GLuint texture = TrackTexture(); + glBindTexture(GL_TEXTURE_1D_ARRAY, texture); + glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + const std::vector seed(static_cast(kExtent) * kOneDArrayLayers * 4, kPoison); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kExtent, kOneDArrayLayers, 0, GL_RGBA, + GL_UNSIGNED_BYTE, seed.data()); + glBindTexture(GL_TEXTURE_1D_ARRAY, 0); + return texture; + } + + // A scratch 2D colour target for the sampling draw. It exists only so the draw has + // somewhere to go that is NOT the layered attachment under test - a draw into that + // would open a render pass and consume the pending clear through LOAD_OP_CLEAR, which + // is the other consumer and the one that was already right. + GLuint MakeScratchColorFbo() { + const GLuint scratch = TrackTexture(); + glBindTexture(GL_TEXTURE_2D, scratch); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kExtent, kExtent); + glBindTexture(GL_TEXTURE_2D, 0); + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scratch, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + return fbo; + } + + // One draw that SAMPLES `texture`, into `intoFbo`. This is what drags the queued clear + // through MaterializePendingClearForTexture (VulkanRenderer's sampled-texture + // pre-pass), which is the consumer that used to write the clear key's layerCount + // straight into a VkImageSubresourceRange. + void DrawSampling(GLuint program, GLuint intoFbo, GLenum textureTarget, GLuint texture) { + glBindFramebuffer(GL_FRAMEBUFFER, intoFbo); + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glActiveTexture(GL_TEXTURE0); + glBindTexture(textureTarget, texture); + glUseProgram(program); + const GLint sourceLocation = glGetUniformLocation(program, "u_source"); + ASSERT_GE(sourceLocation, 0) << "u_source was not reflected"; + glUniform1i(sourceLocation, 0); + const GLint depthLocation = glGetUniformLocation(program, "u_depth"); + ASSERT_GE(depthLocation, 0) << "u_depth was not reflected"; + glUniform1f(depthLocation, 0.0f); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindTexture(textureTarget, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + // Every texel of `texels` is the clear colour. +/-1 per channel, which no rounding can + // exceed and which cannot be confused with the poison (0xAB) it replaced. + void ExpectAllCleared(const std::vector& texels, int perTexelStride, const char* what) { + for (std::size_t i = 0; i < texels.size(); ++i) { + const Rgba8& actual = texels[i]; + const bool ok = std::abs(static_cast(actual.r) - kClearColor.r) <= 1 && + std::abs(static_cast(actual.g) - kClearColor.g) <= 1 && + std::abs(static_cast(actual.b) - kClearColor.b) <= 1; + if (ok) continue; + ADD_FAILURE() << what << ": unit " << (static_cast(i) / perTexelStride) << " texel " + << (static_cast(i) % perTexelStride) << " is " << Describe(actual) + << ", expected " << Describe(kClearColor) + << (actual.r == kPoison && actual.g == kPoison + ? " - the poison, so the clear never reached this one" + : ""); + // One message per unit is enough to say what happened. + i = (static_cast(i) / perTexelStride + 1) * perTexelStride - 1; + } + } + // A depth (or packed depth-stencil) cube-map array of the same shape. No upload: a // depth array is filled by clearing through an attachment, which is the state the // gating cases start from anyway. @@ -708,5 +831,122 @@ void main() Gl().EndFrame(); } + // (6) and (7) leave the render pass alone entirely and pin the OTHER consumer of a layered + // attachment's layer count. + // + // A glClear on a texture-backed FBO with the scissor test off is not executed on the spot: + // it is queued (VkClearManager), and then exactly one of two things consumes it - the next + // render pass's LOAD_OP_CLEAR over the attachment view, or MaterializePendingClearForTexture + // if the texture is used outside a pass first (sampled, blitted, copied, read back). The + // second path writes the queued key's layerCount straight into a VkImageSubresourceRange + // and then POPS the entry, so whatever it misses is lost for good - the render pass never + // gets a second chance at it. + // + // Both consumers must therefore agree about how many layers a layered attachment spans, and + // they are now literally the same function (ResolveAttachmentLayerCount, VkTextureManager.h). + // These two cases are the shapes where a raw `size.z()` and the real answer differ, and + // neither is reachable through the cases above: a cube MAP records the +X face as its + // representative upload target (z = 1, six real faces) and a 1D ARRAY keeps its layer count + // in the state-side height (z = 1, N real layers). The cube-map-ARRAY and 3D shapes the + // earlier cases use both carry their count in z, so they agree either way and cannot see it. + // + // The draw goes into a scratch 2D target, never into the layered attachment, so the + // materialise path is the only consumer that can fire. + TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapClearMaterialisedBySamplingReachesEveryFace) { + if (!Ready()) return; + + const GLuint program = BuildProgram(nullptr, kCubeSampleFragmentSource); + if (program == 0) return; + + const GLuint cube = MakePoisonedCubeMap(); + ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed"; + + const GLuint layeredFbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cube, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube map layered failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered cube-map attachment errored"; + + // Consume the queued clear through the sampled-texture path, with no draw into the + // layered FBO in between. + const GLuint scratchFbo = MakeScratchColorFbo(); + ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete"; + DrawSampling(program, scratchFbo, GL_TEXTURE_CUBE_MAP, cube); + EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored"; + + // Every face, read back one at a time - the per-face spelling is what names the + // offender when only +X was cleared. + static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"}; + glBindTexture(GL_TEXTURE_CUBE_MAP, cube); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + for (int face = 0; face < 6; ++face) { + std::vector texels(static_cast(kExtent) * kExtent, Rgba8{}); + glGetTexImage(static_cast(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, GL_RGBA, + GL_UNSIGNED_BYTE, texels.data()); + EXPECT_EQ(FirstGLError(), 0u) << "reading cube face " << kFaceNames[face] << " back errored"; + ExpectAllCleared(texels, kExtent * kExtent, + (std::string("layered GL_TEXTURE_CUBE_MAP glClear materialised by sampling, " + "face ") + + kFaceNames[face]) + .c_str()); + } + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + + Gl().EndFrame(); + } + + // The 1D-array half of the same divergence. Pre-existing rather than introduced by this + // branch (the clear copy never had ToVulkanLevelExtent), and fixed by the same hoist. + TEST_F(LayeredAttachmentShapeScenario, LayeredOneDArrayClearMaterialisedBySamplingReachesEveryLayer) { + if (!Ready()) return; + + const GLuint program = BuildProgram(nullptr, kOneDArraySampleFragmentSource); + if (program == 0) return; + + const GLuint array = MakePoisoned1DArray(); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no usable GL_TEXTURE_1D_ARRAY on this backend: " << GLErrorName(error); + } + + const GLuint layeredFbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, array, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the 1D array layered failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + // The viewport is the LEVEL's shape: a 1D array level is `kExtent` wide and one row + // tall, whatever its layer count. + glViewport(0, 0, kExtent, 1); + glDisable(GL_SCISSOR_TEST); + glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered 1D-array attachment errored"; + + const GLuint scratchFbo = MakeScratchColorFbo(); + ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete"; + DrawSampling(program, scratchFbo, GL_TEXTURE_1D_ARRAY, array); + EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored"; + + // GL hands a 1D array back as a two-dimensional image whose ROWS are the layers. + std::vector texels(static_cast(kExtent) * kOneDArrayLayers, Rgba8{}); + glBindTexture(GL_TEXTURE_1D_ARRAY, array); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + glBindTexture(GL_TEXTURE_1D_ARRAY, 0); + EXPECT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored"; + ExpectAllCleared(texels, kExtent, + "layered GL_TEXTURE_1D_ARRAY glClear materialised by sampling (unit = layer)"); + + Gl().EndFrame(); + } + } // namespace } // namespace MGITest