diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 40d566b8..278119c1 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -103,6 +103,7 @@ add_executable(MobileGLIntegrationTest Scenarios/TextureViewScenario.cpp Scenarios/PackedWordReadbackScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp + Scenarios/LayeredAttachmentShapeScenario.cpp Scenarios/LayeredTextureReadbackScenario.cpp Scenarios/AtomicCounterScenario.cpp Scenarios/SsboArrayDynamicIndexScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp new file mode 100644 index 00000000..c309a82c --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp @@ -0,0 +1,712 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - THE ATTACHMENT SHAPES A LAYERED FRAMEBUFFER CAN TAKE, AND THE ONE VIEW TYPE +// VULKAN ACCEPTS FOR ALL OF THEM. +// +// glFramebufferTexture on a GL_TEXTURE_3D or a GL_TEXTURE_CUBE_MAP_ARRAY makes a LAYERED +// framebuffer: one attachment that covers every slice / layer-face, addressed by a geometry +// shader writing gl_Layer. Vulkan has exactly one legal spelling for that +// (VUID-VkFramebufferCreateInfo-flags-04113: an attachment view must be VK_IMAGE_VIEW_TYPE_2D +// or _2D_ARRAY), and DirectVulkan used to hand vkCreateFramebuffer the IMAGE's own view type +// instead: +// +// * GL_TEXTURE_3D -> VK_IMAGE_VIEW_TYPE_3D. A 3D image has arrayLayers == 1 and keeps its +// layers on z, so the layer-span guard measured [0, depth) against 1, refused, and returned +// VK_NULL_HANDLE - which then went into pAttachments as a null handle. +// * GL_TEXTURE_CUBE_MAP_ARRAY -> VK_IMAGE_VIEW_TYPE_CUBE_ARRAY. A perfectly valid view, of a +// type no framebuffer may take. The driver dereferenced or rejected it inside +// vkCreateFramebuffer. +// +// Both exits were guarded only by MOBILEGL_ASSERT, which an INFO build (the production and CTS +// default) compiles to nothing - so both were process kills, not wrong pixels: 51 lost QPA +// records over 7 conformance bodies, one runner restart each. +// +// The same function is what routes a NON-layered slice of a 3D texture +// (glFramebufferTextureLayer), and it had the mirror-image hole: it asked for a 3D view there +// 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 +// 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". +// +// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on +// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil +// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment +// sat beside a non-layered depth/stencil one and BOTH backends silently lost the draw. That is +// the shape texture_cube_map_array.stencil_attachments_*_layered and +// geometry_shader.layered_framebuffer.stencil_support are built on, and it is why they fail on +// Espryt as well as crashing on Magma. Case (5) is what found it. +// +// DirectGLES is the control: it hands the same GL calls to the driver, so a red on both backends +// means the scenario is wrong - or the defect is in the shared front end, as it was above - and a +// red on DirectVulkan alone means Magma is. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr int kExtent = 4; + // Four z slices: enough that "only slice 0 was written" and "the whole thing was written" + // are different answers, and small enough that the geometry shader stays well inside + // GL_MAX_GEOMETRY_OUTPUT_VERTICES. + constexpr int k3DSlices = 4; + // Two cubes. One cube would let "attached a single cube" pass; twelve layer-faces would + // not. + constexpr int kCubeLayerFaces = 12; + // The slice a non-layered 3D attachment names. Not 0: slice 0 is the one address that is + // right whether or not the slice is resolved at all. + constexpr int kSubjectSlice = 2; + + // A colour no pass paints, uploaded before every draw. A layer that reads it back was + // never rendered to. + constexpr GLubyte kPoison = 0xAB; + + // 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"). + Rgba8 ExpectedColor(int layer, int pass) { + return {static_cast(10 + layer * 20), static_cast(200 - layer * 10), + static_cast(3 + pass * 60), 255}; + } + + std::string Describe(const Rgba8& color) { + return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + + std::to_string(color.b) + ", " + std::to_string(color.a) + ")"; + } + + // A full-viewport triangle built from gl_VertexID, so nothing here needs a vertex buffer + // and the draw cannot fail for a reason that has nothing to do with the attachment. + // u_depth is the NDC z the whole primitive sits at - the depth/stencil case needs two + // different ones. + const char* const kVertexSource = R"(#version 420 core +uniform float u_depth; +void main() +{ + vec2 corner = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0); + gl_Position = vec4(corner, u_depth, 1.0); +} +)"; + + // The layer count is baked in as a literal rather than passed as a uniform: a + // non-constant loop bound in a geometry shader is legal but is one more thing the + // ESSL transpile could get wrong, and this scenario is not about that. + std::string MakeGeometrySource(int layerCount) { + return "#version 420 core\n" + "layout(triangles) in;\n" + "layout(triangle_strip, max_vertices = " + + std::to_string(layerCount * 3) + + ") out;\n" + "flat out int v_layer;\n" + "void main()\n" + "{\n" + " for (int layer = 0; layer < " + + std::to_string(layerCount) + + "; ++layer) {\n" + " for (int i = 0; i < 3; ++i) {\n" + " gl_Layer = layer;\n" + " v_layer = layer;\n" + " gl_Position = gl_in[i].gl_Position;\n" + " EmitVertex();\n" + " }\n" + " EndPrimitive();\n" + " }\n" + "}\n"; + } + + const char* const kLayeredFragmentSource = R"(#version 420 core +flat in int v_layer; +uniform int u_pass; +out vec4 o_color; +void main() +{ + o_color = vec4(float(10 + v_layer * 20) / 255.0, + float(200 - v_layer * 10) / 255.0, + float(3 + u_pass * 60) / 255.0, + 1.0); +} +)"; + + // The non-layered case has no geometry stage at all - the slice comes from the + // attachment, not from gl_Layer - so it names its layer through a uniform. + const char* const kFlatFragmentSource = R"(#version 420 core +uniform int u_layer; +uniform int u_pass; +out vec4 o_color; +void main() +{ + o_color = vec4(float(10 + u_layer * 20) / 255.0, + float(200 - u_layer * 10) / 255.0, + float(3 + u_pass * 60) / 255.0, + 1.0); +} +)"; + + class LayeredAttachmentShapeScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + DrainErrors(); + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + for (const GLuint fbo : m_fbos) glDeleteFramebuffers(1, &fbo); + m_fbos.clear(); + for (const GLuint texture : m_textures) glDeleteTextures(1, &texture); + m_textures.clear(); + for (const GLuint program : m_programs) glDeleteProgram(program); + m_programs.clear(); + glBindVertexArray(0); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + m_vao = 0; + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + DrainErrors(); + } + + static void DrainErrors() { + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) { + } + } + + // 0 on a DirectGLES driver without GL_EXT_geometry_shader and on a DirectVulkan + // device without the geometryShader feature. The same probe GeometryDrawModeScenario + // and IoBlockNameCollisionScenario use. + static bool BackendHostsGeometry() { + GLint maxGeometryOutputVertices = 0; + glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices); + DrainErrors(); + return maxGeometryOutputVertices >= kCubeLayerFaces * 3; + } + + static std::string InfoLog(GLuint object, bool isShader) { + GLint length = 0; + if (isShader) { + glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length); + } else { + glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length); + } + std::vector buffer(static_cast(length) + 1, '\0'); + if (isShader) { + glGetShaderInfoLog(object, length + 1, nullptr, buffer.data()); + } else { + glGetProgramInfoLog(object, length + 1, nullptr, buffer.data()); + } + return buffer.data(); + } + + // geometrySource may be null, which builds the no-geometry-stage program the + // non-layered case uses. + GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) { + std::vector shaders; + const auto compile = [&](GLenum stage, const char* source) { + const GLuint shader = glCreateShader(stage); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint compiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + shaders.push_back(shader); + if (compiled == GL_FALSE) { + ADD_FAILURE() << "stage 0x" << std::hex << stage << std::dec + << " did not compile: " << InfoLog(shader, true); + return false; + } + return true; + }; + + bool ok = compile(GL_VERTEX_SHADER, kVertexSource); + if (ok && geometrySource != nullptr) ok = compile(GL_GEOMETRY_SHADER, geometrySource); + if (ok) ok = compile(GL_FRAGMENT_SHADER, fragmentSource); + if (!ok) { + for (const GLuint shader : shaders) glDeleteShader(shader); + return 0; + } + + const GLuint program = glCreateProgram(); + for (const GLuint shader : shaders) glAttachShader(program, shader); + glLinkProgram(program); + for (const GLuint shader : shaders) glDeleteShader(shader); + GLint linked = 0; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked == GL_FALSE) { + ADD_FAILURE() << "the program did not link: " << InfoLog(program, false); + glDeleteProgram(program); + return 0; + } + m_programs.push_back(program); + return program; + } + + GLuint TrackTexture() { + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + return texture; + } + + GLuint TrackFramebuffer() { + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + m_fbos.push_back(fbo); + return fbo; + } + + // An RGBA8 3D texture, every texel poisoned. + GLuint MakePoisoned3DColor() { + const GLuint texture = TrackTexture(); + glBindTexture(GL_TEXTURE_3D, texture); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kExtent, kExtent, k3DSlices); + const std::vector seed( + static_cast(kExtent) * kExtent * k3DSlices * 4, kPoison); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kExtent, kExtent, k3DSlices, GL_RGBA, + GL_UNSIGNED_BYTE, seed.data()); + glBindTexture(GL_TEXTURE_3D, 0); + return texture; + } + + // An RGBA8 cube-map array of kCubeLayerFaces layer-faces, every texel poisoned. + GLuint MakePoisonedCubeArrayColor() { + const GLuint texture = TrackTexture(); + glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture); + glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, kExtent, kExtent, kCubeLayerFaces); + const std::vector seed( + static_cast(kExtent) * kExtent * kCubeLayerFaces * 4, kPoison); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent, kCubeLayerFaces, + GL_RGBA, GL_UNSIGNED_BYTE, seed.data()); + glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0); + return texture; + } + + // 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. + GLuint MakeCubeArrayDepth(GLenum internalFormat) { + const GLuint texture = TrackTexture(); + glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture); + glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent, kCubeLayerFaces); + glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0); + return texture; + } + + // glGetTexImage rather than a per-layer glReadPixels: a cube-map array has no + // per-layer attachment on every backend, and glGetTexImage is the readback both of + // them answer for whole-level layered targets (LayeredTextureReadbackScenario pins + // that contract). It is a real GPU readback on DirectVulkan - the texture manager + // copies the image into a staging buffer - so a stale CPU shadow cannot pass it. + std::vector ReadLevel(GLenum target, GLuint texture, int layers) { + std::vector texels(static_cast(kExtent) * kExtent * layers, Rgba8{}); + glBindTexture(target, texture); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glGetTexImage(target, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + glBindTexture(target, 0); + return texels; + } + + // Every texel of every layer must be that layer's expected colour. Reported per layer + // so a failure names which one, and the poison is called out by name. + void ExpectEveryLayer(const std::vector& texels, int layers, int pass, const char* what) { + for (int layer = 0; layer < layers; ++layer) { + const Rgba8 expected = ExpectedColor(layer, pass); + for (int y = 0; y < kExtent; ++y) { + for (int x = 0; x < kExtent; ++x) { + const std::size_t index = + (static_cast(layer) * kExtent + y) * kExtent + x; + const Rgba8 actual = texels[index]; + if (actual == expected) continue; + ADD_FAILURE() + << what << ": layer " << layer << " texel (" << x << ", " << y << ") is " + << Describe(actual) << ", expected " << Describe(expected) + << (actual.r == kPoison && actual.g == kPoison + ? " - the poison, so nothing was ever rendered into this layer" + : ""); + // One message per layer is enough to say what happened. + y = kExtent; + break; + } + } + } + } + + ::testing::AssertionResult FramebufferIsComplete() { + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess(); + return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status; + } + + // One layered pass over the whole attachment. + void DrawLayered(GLuint program, int pass, float depth) { + glUseProgram(program); + const GLint passLocation = glGetUniformLocation(program, "u_pass"); + ASSERT_GE(passLocation, 0) << "u_pass was not reflected"; + glUniform1i(passLocation, pass); + const GLint depthLocation = glGetUniformLocation(program, "u_depth"); + ASSERT_GE(depthLocation, 0) << "u_depth was not reflected"; + glUniform1f(depthLocation, depth); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + + GLuint m_vao = 0; + std::vector m_textures; + std::vector m_fbos; + std::vector m_programs; + }; + + // (1) A LAYERED GL_TEXTURE_3D colour attachment. Pre-fix this is the null VkImageView: + // the attachment asked for a 3D view, whose [0, 4) layer span was measured against the + // image's arrayLayers == 1 and refused, and VK_NULL_HANDLE went to vkCreateFramebuffer. + TEST_F(LayeredAttachmentShapeScenario, LayeredThreeDColorAttachmentReachesEverySlice) { + if (!Ready()) return; + if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer"; + + const std::string geometrySource = MakeGeometrySource(k3DSlices); + const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource); + if (program == 0) return; + + const GLuint color = MakePoisoned3DColor(); + ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed"; + + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the 3D texture layered failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + DrawLayered(program, /*pass=*/0, /*depth=*/0.0f); + EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices); + EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored"; + ExpectEveryLayer(texels, k3DSlices, /*pass=*/0, "layered GL_TEXTURE_3D colour attachment"); + + Gl().EndFrame(); + } + + // (2) The same texture attached ONE SLICE at a time, which is the other half of the same + // view-type decision. Pre-fix a non-layered 3D attachment also asked for a 3D view, so + // the per-slice branch never ran and slice 2 resolved to VK_NULL_HANDLE. Needs no + // geometry stage - the slice comes from the attachment. + TEST_F(LayeredAttachmentShapeScenario, NonLayeredThreeDSliceAttachmentWritesOnlyThatSlice) { + if (!Ready()) return; + + const GLuint program = BuildProgram(nullptr, kFlatFragmentSource); + if (program == 0) return; + + const GLuint color = MakePoisoned3DColor(); + ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed"; + + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0, kSubjectSlice); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching slice " << kSubjectSlice << " failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glUseProgram(program); + const GLint layerLocation = glGetUniformLocation(program, "u_layer"); + const GLint passLocation = glGetUniformLocation(program, "u_pass"); + const GLint depthLocation = glGetUniformLocation(program, "u_depth"); + ASSERT_GE(layerLocation, 0); + ASSERT_GE(passLocation, 0); + ASSERT_GE(depthLocation, 0); + glUniform1i(layerLocation, kSubjectSlice); + glUniform1i(passLocation, 0); + glUniform1f(depthLocation, 0.0f); + glDrawArrays(GL_TRIANGLES, 0, 3); + EXPECT_EQ(FirstGLError(), 0u) << "the per-slice draw errored"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices); + EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored"; + + const Rgba8 expected = ExpectedColor(kSubjectSlice, 0); + const Rgba8 poison{kPoison, kPoison, kPoison, kPoison}; + for (int slice = 0; slice < k3DSlices; ++slice) { + const Rgba8& target = (slice == kSubjectSlice) ? expected : poison; + for (int y = 0; y < kExtent; ++y) { + for (int x = 0; x < kExtent; ++x) { + const std::size_t index = + (static_cast(slice) * kExtent + y) * kExtent + x; + const Rgba8 actual = texels[index]; + if (actual == target) continue; + ADD_FAILURE() << "slice " << slice << " texel (" << x << ", " << y << ") is " + << Describe(actual) << ", expected " << Describe(target) + << (slice == kSubjectSlice + ? " - the attached slice was not the one written" + : " - a slice the attachment did not name was written"); + y = kExtent; + break; + } + } + } + + Gl().EndFrame(); + } + + // (3) A LAYERED GL_TEXTURE_CUBE_MAP_ARRAY colour attachment. Pre-fix this is the other + // exit: a valid CUBE_ARRAY view of a type no framebuffer may take, handed straight to + // vkCreateFramebuffer. + TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayColorAttachmentReachesEveryLayerFace) { + if (!Ready()) return; + if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer"; + + const GLuint color = MakePoisonedCubeArrayColor(); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error); + } + + const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces); + const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource); + if (program == 0) return; + + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube-map array layered failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + DrawLayered(program, /*pass=*/0, /*depth=*/0.0f); + EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces); + EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored"; + ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0, + "layered GL_TEXTURE_CUBE_MAP_ARRAY colour attachment"); + + Gl().EndFrame(); + } + + // (4) A layered cube-map-array DEPTH attachment, proved to have covered every layer-face: + // + // pass 0 paints at z = 0 against a depth buffer cleared to 1; + // pass 1 paints at z = +0.5, which GL_LESS must reject. + // + // A layer that reads back pass 1's colour is a layer the depth attachment never covered - + // which is exactly what attaching one layer-face of it, or none, looks like. Runs on both + // backends: this is the cross-backend control for the packed case below. + TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthAttachmentGatesEveryLayerFace) { + if (!Ready()) return; + if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer"; + + const GLuint color = MakePoisonedCubeArrayColor(); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error); + } + + const GLuint depth = MakeCubeArrayDepth(GL_DEPTH_COMPONENT24); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no depth GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error); + } + + const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces); + const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource); + if (program == 0) return; + + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth pair failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + glDepthMask(GL_TRUE); + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth attachment errored"; + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + DrawLayered(program, /*pass=*/0, /*depth=*/0.0f); + DrawLayered(program, /*pass=*/1, /*depth=*/0.5f); // farther: GL_LESS must reject it + EXPECT_EQ(FirstGLError(), 0u) << "the two layered draws errored"; + + glDisable(GL_DEPTH_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces); + EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored"; + ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0, + "layered cube-map-array depth attachment (pass 1's colour on a layer means the " + "depth test did not cover it)"); + + Gl().EndFrame(); + } + + // (5) The PACKED depth-stencil shape the conformance suite crashes on: + // texture_cube_map_array.stencil_attachments_*_layered attaches a cube-map array as COLOR0 + // AND the same-shaped GL_DEPTH24_STENCIL8 array as GL_DEPTH_STENCIL_ATTACHMENT, both + // layered. Both aspects are proved to have covered every layer-face: + // + // pass 0 paints at z = 0 with the stencil op writing 1; + // pass 1 paints at z = +0.5, which the depth test must reject; + // pass 2 paints with the depth test off but a stencil func of EQUAL 0, which the + // stencil written by pass 0 must reject. + // + // The probe in front of the gating is where this scenario earned its keep. The attachment + // point ITSELF was broken: glFramebufferTexture(GL_DEPTH_STENCIL_ATTACHMENT) is a + // shorthand that the front end splits into a depth and a stencil attachment, and the split + // dropped the call's `layered` flag (GL_Framebuffer.cpp, + // AttachFramebufferTextureWithUploadTarget). A layered colour attachment therefore sat + // beside a NON-layered depth/stencil one, and both backends lost the draw entirely - with + // no GL error and glCheckFramebufferStatus answering COMPLETE. DirectVulkan built the + // depth/stencil view with layerCount 1 under a framebuffer declaring 12 layers + // (VUID-VkFramebufferCreateInfo-flags-04535, which the validation layers report on this + // exact case); DirectGLES attached one layer of it beside a layered colour target, which + // the driver answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS. Case (4) above is what + // isolates it to the attachment point: the same cube-map array on GL_DEPTH_ATTACHMENT + // rendered and gated correctly throughout. + // + // So the probe stays, as an assertion rather than as scaffolding: it turns that regression + // back into ONE message about the shape instead of twelve about individual layers. + TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthStencilAttachmentGatesEveryLayerFace) { + if (!Ready()) return; + if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer"; + + const GLuint color = MakePoisonedCubeArrayColor(); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error); + } + + const GLuint depthStencil = MakeCubeArrayDepth(GL_DEPTH24_STENCIL8); + if (const GLenum error = FirstGLError()) { + GTEST_SKIP() << "no depth-stencil GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " + << GLErrorName(error); + } + + const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces); + const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource); + if (program == 0) return; + + glViewport(0, 0, kExtent, kExtent); + glDisable(GL_SCISSOR_TEST); + + // The probe: its own colour attachment (so the subject texture keeps its poison), the + // same depth-stencil attachment, and both tests off - so every layer-face must come + // back painted, whatever the gating below then decides. + { + const GLuint probeColor = MakePoisonedCubeArrayColor(); + const GLuint probeFbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, probeFbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, probeColor, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed"; + ASSERT_TRUE(FramebufferIsComplete()); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + DrawLayered(program, /*pass=*/3, /*depth=*/0.0f); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector probeTexels = + ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, probeColor, kCubeLayerFaces); + EXPECT_EQ(FirstGLError(), 0u) << "the probe draw or readback errored"; + ExpectEveryLayer(probeTexels, kCubeLayerFaces, /*pass=*/3, + "a layered draw with the depth and stencil tests DISABLED, into a colour + " + "GL_DEPTH_STENCIL_ATTACHMENT cube-map-array pair (all poison means the " + "attachment pair lost the draw outright, which is what a non-layered " + "depth/stencil attachment beside a layered colour one looks like)"); + // The gating assertions below can only add noise once the shape itself is broken. + if (::testing::Test::HasNonfatalFailure()) return; + } + + const GLuint fbo = TrackFramebuffer(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed"; + ASSERT_TRUE(FramebufferIsComplete()); + + glDepthMask(GL_TRUE); + glStencilMask(0xFFu); + glClearDepth(1.0); + glClearStencil(0); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth-stencil attachment errored"; + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 1, 0xFFu); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + DrawLayered(program, /*pass=*/0, /*depth=*/0.0f); + + // Farther than pass 0, so GL_LESS must reject it on every layer. + glStencilFunc(GL_ALWAYS, 1, 0xFFu); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + DrawLayered(program, /*pass=*/1, /*depth=*/0.5f); + + // Depth out of the way; only the stencil pass 0 wrote can reject this one. + glDepthFunc(GL_ALWAYS); + glStencilFunc(GL_EQUAL, 0, 0xFFu); + DrawLayered(program, /*pass=*/2, /*depth=*/-0.5f); + EXPECT_EQ(FirstGLError(), 0u) << "the three layered draws errored"; + + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const std::vector texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces); + EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored"; + ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0, + "layered cube-map-array depth-stencil attachment (a later pass's colour means " + "the depth or stencil test did not cover that layer)"); + + Gl().EndFrame(); + } + + } // namespace +} // namespace MGITest