// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.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 - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
//
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
// That is what it did here, on both backends, for two waves.
//
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
// ONE texture and checks the value that comes back, so a failure names the target kind and the
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
// bottom still does - a defect that only appears when several kinds share a program is invisible
// to the single-kind cases by construction.
//
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
//
//   * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
//   * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
//     binding, with the image unit then assigned by glUniform1i. That combination is the one ES
//     cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
//     has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
//   * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
//     frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
//     for a reason the conformance case excludes;
//   * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
//     to pass, and which is where a target kind whose layeredness a backend does not recognise
//     goes wrong.
//
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
// conformance case never asks it for a multisample image at all. The two cases below are kept
// and skip on that same query, so the coverage is already written the day a backend advertises
// them - and so the skip is a standing record of WHY the conformance case passes without them.

#include <algorithm>
#include <cstddef>
#include <string>
#include <vector>

#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"

#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES

namespace MGITest {
    namespace {

        // The conformance case's own dimensions: one level, 6 on every axis (which is also
        // exactly one cube's worth for a cube array), and a single texel read at the origin.
        constexpr int kExtent = 6;
        constexpr GLuint kFilledValue = 7u;
        constexpr GLuint kStoredValue = 13u;
        // What the atomic cases add to a filled texel. Distinct from both values above, so a
        // wrong answer cannot be read as either the untouched fill or a plain store.
        constexpr GLuint kAtomicAddend = 5u;

        // Everything that differs between the eleven kinds, in one row.
        struct TargetKind {
            const char* name;      // this scenario's name for it, which failure messages carry
            GLenum target;         // the GL texture target
            const char* imageType; // the GLSL image uniform type
            const char* coord;     // the coordinate expression imageLoad/imageStore takes
            bool multisample;      // needs GL_MAX_IMAGE_SAMPLES > 0
            bool buffer;           // storage comes from a buffer object, not TexStorage
        };

        constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
        constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
                                          false};
        constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
        constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
                                          false};
        constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
        constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
        constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
        constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
                                            "ivec3(0, 0, 0)", false, false};
        constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
        constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
        constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
                                            "ivec3(0, 0, 0)", true, false};

        // A multisample image load/store takes the sample index as an extra argument; no other
        // kind does. Keeping that in one place stops the two spellings drifting apart.
        std::string LoadExpression(const TargetKind& kind, const std::string& name) {
            return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
        }

        std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
            return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
                   value + ", 0, 0, 0));";
        }

        const char* kComputePrologue = "#version 440 core\n"
                                       "\n"
                                       "layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
                                       "\n";

        const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
                                   "    uint sum;\n"
                                   "} ssb;\n"
                                   "\n";

        // The conformance case's shader, narrowed to a single image.
        std::string SingleLoadSource(const TargetKind& kind) {
            return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
                   " i0;\n" + kResultBlock + "void main()\n{\n    uvec4 v = " + LoadExpression(kind, "i0") +
                   ";\n    ssb.sum = v.r;\n}\n";
        }

        // The other direction. Written as its own program rather than a read-write one so that a
        // backend which gets the store right and the load wrong (or the reverse) is not able to
        // cancel its own defect out.
        std::string SingleStoreSource(const TargetKind& kind) {
            return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
                   kind.imageType + " i0;\n\nvoid main()\n{\n    " + StoreStatement(kind, "i0", "13u") + "\n}\n";
        }

        // The third direction, and the one neither of the two above can stand in for: an
        // imageAtomic* reaches its texel through a SPIR-V operand path of its own
        // (OpImageTexelPointer), not through OpImageRead or OpImageWrite. SPIRV-Cross's "ES has
        // no 1D image, address it as 2D" coordinate widening is applied on the read and write
        // paths and NOT on that one, so a 1D image whose loads and stores are both correct could
        // still lose its entire stage to a single imageAtomicAdd - which is what
        // KHR-GL4x.shader_image_load_store.basic-allTargets-atomic measured, with the driver
        // answering "'imageAtomicAdd' : no matching overloaded function found".
        //
        // No readonly/writeonly here: an atomic needs both directions, and r32ui is one of the
        // three formats GLSL ES exempts from the qualifier rule, so the bare declaration is legal.
        // Returns the value the texel held BEFORE the add, so one dispatch checks the atomic's
        // return value and the load case that follows checks its memory effect.
        std::string SingleAtomicSource(const TargetKind& kind) {
            return std::string(kComputePrologue) + "layout (location = 0, r32ui) coherent uniform " +
                   kind.imageType + " i0;\n" + kResultBlock + "void main()\n{\n    ssb.sum = imageAtomicAdd(i0, " +
                   kind.coord + (kind.multisample ? ", 0, " : ", ") + std::to_string(kAtomicAddend) + "u);\n}\n";
        }

        class ImageTargetKindScenario : public ScenarioTest {
        protected:
            void TearDown() override {
                if (!Ready()) return;
                glUseProgram(0);
                for (GLuint p : m_programs) glDeleteProgram(p);
                for (GLuint t : m_textures) glDeleteTextures(1, &t);
                for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
                m_programs.clear();
                m_textures.clear();
                m_buffers.clear();
                // Leave no image unit bound. These scenarios share one context, and a stale image
                // binding is exactly the kind of state that makes the NEXT scenario's failure
                // impossible to reproduce on its own.
                GLint maxImageUnits = 0;
                glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
                for (GLint unit = 0; unit < maxImageUnits; ++unit) {
                    glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
                }
                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
                while (glGetError() != GL_NO_ERROR) {
                }
            }

            bool ImagesAreUsable() const {
                GLint maxImageUnits = 0;
                glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
                GLint maxComputeImageUniforms = 0;
                glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
                while (glGetError() != GL_NO_ERROR) {
                }
                return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
            }

            // The conformance case's own multisample gate, asked the same way it asks it.
            bool MultisampleImagesAreUsable() const {
                GLint maxImageSamples = 0;
                glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
                while (glGetError() != GL_NO_ERROR) {
                }
                return maxImageSamples > 0;
            }

            GLuint MakeComputeProgram(const std::string& source) {
                const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
                const char* text = source.c_str();
                glShaderSource(shader, 1, &text, nullptr);
                glCompileShader(shader);
                GLint compiled = GL_FALSE;
                glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
                if (compiled == GL_FALSE) {
                    char log[4096] = {};
                    glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
                    ADD_FAILURE() << "the compute shader did not compile: " << log << "\nsource:\n" << source;
                    glDeleteShader(shader);
                    return 0;
                }
                const GLuint program = glCreateProgram();
                m_programs.push_back(program);
                glAttachShader(program, shader);
                glLinkProgram(program);
                glDeleteShader(shader);
                GLint linked = GL_FALSE;
                glGetProgramiv(program, GL_LINK_STATUS, &linked);
                if (linked == GL_FALSE) {
                    char log[4096] = {};
                    glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
                    ADD_FAILURE() << "the compute program did not link: " << log << "\nsource:\n" << source;
                    return 0;
                }
                return program;
            }

            // Storage plus a full fill with `value`, in the spelling each target kind needs.
            // Returns 0 - having already reported - when the target could not be created.
            GLuint MakeTexture(const TargetKind& kind, bool fill, GLuint value = kFilledValue) {
                const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, value);

                if (kind.buffer) {
                    GLuint buffer = 0;
                    glGenBuffers(1, &buffer);
                    m_buffers.push_back(buffer);
                    glBindBuffer(GL_TEXTURE_BUFFER, buffer);
                    glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
                                 fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
                    GLuint texture = 0;
                    glGenTextures(1, &texture);
                    m_textures.push_back(texture);
                    glBindTexture(GL_TEXTURE_BUFFER, texture);
                    glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
                    if (const GLenum error = FirstGLError()) {
                        ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
                                      << GLErrorName(error);
                        return 0;
                    }
                    return texture;
                }

                GLuint texture = 0;
                glGenTextures(1, &texture);
                m_textures.push_back(texture);
                glBindTexture(kind.target, texture);

                switch (kind.target) {
                case GL_TEXTURE_1D:
                    glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
                    break;
                case GL_TEXTURE_2D:
                case GL_TEXTURE_RECTANGLE:
                case GL_TEXTURE_1D_ARRAY:
                case GL_TEXTURE_CUBE_MAP:
                    glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
                    break;
                case GL_TEXTURE_2D_ARRAY:
                case GL_TEXTURE_3D:
                case GL_TEXTURE_CUBE_MAP_ARRAY:
                    glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
                    break;
                case GL_TEXTURE_2D_MULTISAMPLE:
                    glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
                    break;
                case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
                    glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
                    break;
                default:
                    ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
                    return 0;
                }
                if (const GLenum error = FirstGLError()) {
                    ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
                    return 0;
                }

                // A multisample texture has no TexSubImage - the conformance case fills it with a
                // compute pass, which is what the store cases below do.
                if (!fill || kind.multisample) return texture;

                switch (kind.target) {
                case GL_TEXTURE_1D:
                    glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
                    break;
                case GL_TEXTURE_2D:
                case GL_TEXTURE_RECTANGLE:
                case GL_TEXTURE_1D_ARRAY:
                    glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
                                    texels.data());
                    break;
                case GL_TEXTURE_CUBE_MAP:
                    for (int face = 0; face < 6; ++face) {
                        glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
                                        kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
                    }
                    break;
                case GL_TEXTURE_2D_ARRAY:
                case GL_TEXTURE_3D:
                case GL_TEXTURE_CUBE_MAP_ARRAY:
                    glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
                                    GL_UNSIGNED_INT, texels.data());
                    break;
                default:
                    break;
                }
                if (const GLenum error = FirstGLError()) {
                    ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
                    return 0;
                }
                return texture;
            }

            // A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
            GLuint MakeResultBuffer() {
                GLuint ssbo = 0;
                glGenBuffers(1, &ssbo);
                m_buffers.push_back(ssbo);
                const GLuint zero = 0u;
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
                glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
                return ssbo;
            }

            GLuint ReadResult(GLuint ssbo) {
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
                GLuint value = 0xFFFFFFFFu;
                glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
                return value;
            }

            // Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
            // compute dispatch, and require the value back.
            void RunLoadCase(const TargetKind& kind) {
                const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
                if (program == 0) return;
                const GLuint texture = MakeTexture(kind, true);
                if (texture == 0) return;
                const GLuint ssbo = MakeResultBuffer();

                glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
                ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";

                glUseProgram(program);
                // The unit, by LOCATION - the conformance case's own redundant-but-legal
                // assignment, and the one ES cannot take at the API level.
                glUniform1i(0, 0);
                ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";

                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_ALL_BARRIER_BITS);
                EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";

                EXPECT_EQ(ReadResult(ssbo), kFilledValue)
                    << kind.name << ": the compute dispatch did not read the value the texture was filled with";
                glUseProgram(0);
            }

            // The other direction: store through an image uniform, then read the same texel back
            // through a SECOND program, so a defect cannot cancel itself out.
            void RunStoreCase(const TargetKind& kind) {
                const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
                const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
                if (storeProgram == 0 || loadProgram == 0) return;
                const GLuint texture = MakeTexture(kind, false);
                if (texture == 0) return;
                const GLuint ssbo = MakeResultBuffer();

                glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
                ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";

                glUseProgram(storeProgram);
                glUniform1i(0, 0);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_ALL_BARRIER_BITS);
                EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";

                glUseProgram(loadProgram);
                glUniform1i(0, 0);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_ALL_BARRIER_BITS);
                EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";

                EXPECT_EQ(ReadResult(ssbo), kStoredValue)
                    << kind.name << ": the value stored through the image did not come back";
                glUseProgram(0);
            }

            // Fill a texture of `kind`, add to texel (0,0,0) atomically, and require BOTH the
            // value the atomic returned and the value it left behind. The read-back runs as a
            // second program, for the same reason the store case does: a backend that gets the
            // atomic's return right and its memory effect wrong cannot cancel itself out.
            void RunAtomicCase(const TargetKind& kind) {
                const GLuint atomicProgram = MakeComputeProgram(SingleAtomicSource(kind));
                const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
                if (atomicProgram == 0 || loadProgram == 0) return;
                const GLuint texture = MakeTexture(kind, true);
                if (texture == 0) return;
                const GLuint ssbo = MakeResultBuffer();

                glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
                ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";

                glUseProgram(atomicProgram);
                glUniform1i(0, 0);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_ALL_BARRIER_BITS);
                EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the atomic dispatch leaked a GL error";
                EXPECT_EQ(ReadResult(ssbo), kFilledValue)
                    << kind.name << ": imageAtomicAdd did not return the value the texel held before it";

                glUseProgram(loadProgram);
                glUniform1i(0, 0);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_ALL_BARRIER_BITS);
                EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
                EXPECT_EQ(ReadResult(ssbo), kFilledValue + kAtomicAddend)
                    << kind.name << ": imageAtomicAdd did not leave the sum in the texel";
                glUseProgram(0);
            }

            // The same texture, bound four times over, varying nothing but `layered` and `layer`.
            //
            // GL 4.6 core 8.26 (and ES 3.2 8.22, word for word): "If the texture identified by
            // texture does not have multiple layers or faces, the entire texture level is bound,
            // regardless of the values of layered and layer." REGARDLESS means ignored - not
            // clamped, and not an error - so every one of the four rows has to read the same texel
            // out of a target that has no layers, including the two rows that name layer 1 on a
            // texture whose only layer is 0. DirectGLES used to normalize `layered` and forward
            // `layer` verbatim; Adreno honours the bogus layer by leaving the image unit reading
            // zero, which is exactly the two rows KHR-GL42.bind_image_texture.single_layer failed.
            //
            // The bindings are checked back as well, because the fix depends on WHERE the
            // normalization happens: the frontend shadow must keep echoing the application's own
            // values (gl4cShaderImageLoadStoreTests' CheckBinding compares them exactly), and only
            // the backend's driver call may drop the layer.
            void RunNonLayerableLayerSweepCase(const TargetKind& kind) {
                const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
                if (program == 0) return;
                const GLuint texture = MakeTexture(kind, true);
                if (texture == 0) return;

                // A multisample texture has no TexSubImage, so MakeTexture leaves it unwritten and
                // it is seeded the way the store cases do it - through a dispatch of its own.
                const GLuint expected = kind.multisample ? kStoredValue : kFilledValue;
                if (kind.multisample) {
                    const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
                    if (storeProgram == 0) return;
                    glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
                    glUseProgram(storeProgram);
                    glUniform1i(0, 0);
                    glDispatchCompute(1, 1, 1);
                    glMemoryBarrier(GL_ALL_BARRIER_BITS);
                    ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": seeding the multisample texture errored";
                }

                const GLuint ssbo = MakeResultBuffer();
                glUseProgram(program);
                glUniform1i(0, 0);
                ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";

                // glcBindImageTextureTests' own four rows, in its own order.
                struct LayerRow {
                    GLboolean layered;
                    GLint layer;
                };
                static constexpr LayerRow kRows[] = {{GL_TRUE, 1}, {GL_TRUE, 0}, {GL_FALSE, 1}, {GL_FALSE, 0}};

                for (const LayerRow& row : kRows) {
                    const std::string where = std::string(kind.name) +
                                              ": layered=" + (row.layered == GL_TRUE ? "TRUE" : "FALSE") +
                                              " layer=" + std::to_string(row.layer);
                    // Re-zeroed per row, so a row whose binding reads nothing cannot pass on the
                    // previous row's answer.
                    const GLuint zero = 0u;
                    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
                    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &zero);

                    glBindImageTexture(0, texture, 0, row.layered, row.layer, GL_READ_ONLY, GL_R32UI);
                    EXPECT_EQ(FirstGLError(), 0u) << where << ": glBindImageTexture errored";

                    GLint reportedLayered = -1;
                    GLint reportedLayer = -1;
                    glGetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &reportedLayered);
                    glGetIntegeri_v(GL_IMAGE_BINDING_LAYER, 0, &reportedLayer);
                    EXPECT_EQ(reportedLayered, row.layered == GL_TRUE ? 1 : 0)
                        << where << ": GL_IMAGE_BINDING_LAYERED stopped reporting the application's value";
                    EXPECT_EQ(reportedLayer, row.layer)
                        << where << ": GL_IMAGE_BINDING_LAYER stopped reporting the application's value";

                    glDispatchCompute(1, 1, 1);
                    glMemoryBarrier(GL_ALL_BARRIER_BITS);
                    EXPECT_EQ(FirstGLError(), 0u) << where << ": the dispatch leaked a GL error";
                    EXPECT_EQ(ReadResult(ssbo), expected)
                        << where
                        << ": the texel did not come back, so the binding named a layer the texture "
                           "does not have instead of the whole level";
                }
                glUseProgram(0);
            }

            std::vector<GLuint> m_programs;
            std::vector<GLuint> m_textures;
            std::vector<GLuint> m_buffers;
        };

    } // namespace

    // ---- the load direction, one target kind per case -----------------------
    //
    // Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
    // failure names the kind.

#define MGL_DEFINE_LOAD_CASE(CaseName, Kind)                                                                    \
    TEST_F(ImageTargetKindScenario, Loads##CaseName) {                                                          \
        if (!Ready()) return;                                                                                   \
        if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";                                    \
        if ((Kind).multisample && !MultisampleImagesAreUsable()) {                                              \
            GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image "   \
                            "here and never asks for a multisample one";                                        \
        }                                                                                                       \
        RunLoadCase(Kind);                                                                                      \
    }

#define MGL_DEFINE_STORE_CASE(CaseName, Kind)                                                                   \
    TEST_F(ImageTargetKindScenario, Stores##CaseName) {                                                         \
        if (!Ready()) return;                                                                                   \
        if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";                                    \
        if ((Kind).multisample && !MultisampleImagesAreUsable()) {                                              \
            GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image "   \
                            "here and never asks for a multisample one";                                        \
        }                                                                                                       \
        RunStoreCase(Kind);                                                                                      \
    }

    MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
    MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
    MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
    MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
    MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
    MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
    MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
    MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
    MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
    MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
    MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)

    MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
    MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
    MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
    MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
    MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
    MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
    MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
    MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
    MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
    MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
    MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)

#undef MGL_DEFINE_LOAD_CASE
#undef MGL_DEFINE_STORE_CASE

    // ---- and the atomic direction, on the two kinds ES has to emulate -------
    //
    // Deliberately NOT every kind. imageAtomic* takes its own SPIR-V operand path
    // (OpImageTexelPointer), and the only kinds whose coordinate that path has to RESHAPE are the
    // two 1D ones - everything else addresses its ES texture with the coordinate the application
    // wrote. GL_TEXTURE_1D_ARRAY is the control (its reshape has been in
    // Lower1DArrayImagesForEssl from the start, and basic-allTargets-atomic passes on it);
    // GL_TEXTURE_1D is the one that had none, so `imageAtomicAdd(g_image_1d, coord.x, 2)` reached
    // the driver as a scalar against an iimage2D and took the whole fragment stage - and its six
    // other images - with it.

#define MGL_DEFINE_ATOMIC_CASE(CaseName, Kind)                                                                  \
    TEST_F(ImageTargetKindScenario, AtomicallyAddsTo##CaseName) {                                               \
        if (!Ready()) return;                                                                                   \
        if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";                                    \
        RunAtomicCase(Kind);                                                                                    \
    }

    MGL_DEFINE_ATOMIC_CASE(Texture1D, kKind1D)
    MGL_DEFINE_ATOMIC_CASE(Texture1DArray, kKind1DArray)

#undef MGL_DEFINE_ATOMIC_CASE

    // ---- and the same texture bound four times, varying only layered/layer ---
    //
    // KHR-GL42.bind_image_texture.single_layer's sweep, on the kinds whose backend target has
    // neither layers nor faces. Two of its four rows name layer 1 on a single-layer texture,
    // which the spec says is to be ignored outright rather than honoured or rejected - and
    // which DirectGLES used to forward to the ES driver as written.

#define MGL_DEFINE_LAYER_SWEEP_CASE(CaseName, Kind)                                                             \
    TEST_F(ImageTargetKindScenario, IgnoresLayerFor##CaseName) {                                                \
        if (!Ready()) return;                                                                                   \
        if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";                                    \
        if ((Kind).multisample && !MultisampleImagesAreUsable()) {                                              \
            GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image "  \
                            "here and never asks for a multisample one";                                        \
        }                                                                                                       \
        RunNonLayerableLayerSweepCase(Kind);                                                                    \
    }

    MGL_DEFINE_LAYER_SWEEP_CASE(Texture2D, kKind2D)
    MGL_DEFINE_LAYER_SWEEP_CASE(Texture1D, kKind1D)
    MGL_DEFINE_LAYER_SWEEP_CASE(TextureRectangle, kKindRect)
    MGL_DEFINE_LAYER_SWEEP_CASE(Texture2DMultisample, kKind2DMS)

#undef MGL_DEFINE_LAYER_SWEEP_CASE

    // ---- and all of them at once -------------------------------------------
    //
    // The conformance case's actual shape. The single-kind cases above cannot see a defect that
    // needs several kinds in one program - a binding remap that only collides when two image
    // types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
    // - and that class of defect is precisely what "each kind passes alone but the case still
    // fails" would mean.
    //
    // Each unit is filled with its own DISTINCT value rather than a shared one, so a shortfall
    // names WHICH kind is missing rather than merely how many are: with one shared value, "three
    // kinds read zero" and "one kind read zero" differ only by a multiple, and any two kinds are
    // interchangeable in the total. A sum still cannot see two kinds SWAPPING - addition is
    // commutative, and the conformance case has exactly the same blind spot - but the single-kind
    // cases above pin each kind to its own texture already, so a swap cannot hide there.
    TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
        if (!Ready()) return;
        if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";

        // The two kinds this whole scenario file exists for come FIRST, and that ordering is
        // load-bearing rather than cosmetic. The list has to be truncated to the device's image
        // unit count, and the guaranteed minimum is small - ES 3.1 promises only four compute
        // image uniforms - so a list in the conformance case's own order would put imageBuffer
        // at index five and drop it on exactly the devices most likely to get it wrong. A test
        // that quietly stops covering its own subject is worse than one that fails.
        const bool multisample = MultisampleImagesAreUsable();
        std::vector<TargetKind> kinds{kKind1DArray, kKindBuffer, kKind2D,   kKind1D,      kKind2DArray,
                                      kKind3D,      kKindCube,   kKindRect, kKindCubeArray};
        if (multisample) {
            kinds.push_back(kKind2DMS);
            kinds.push_back(kKind2DMSArray);
        }

        GLint maxComputeImageUniforms = 0;
        glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
        GLint maxImageUnits = 0;
        glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
        while (glGetError() != GL_NO_ERROR) {
        }
        const std::size_t count =
            std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
                                                                                              maxImageUnits))));
        if (count == 0) GTEST_SKIP() << "no image units";
        // Named, not silently dropped: `expected` is computed over whatever survives, so a
        // truncated run is self-consistently green and would otherwise never say what it stopped
        // covering.
        if (count < kinds.size()) {
            std::string dropped;
            for (std::size_t i = count; i < kinds.size(); ++i) {
                if (!dropped.empty()) dropped += ", ";
                dropped += kinds[i].name;
            }
            RecordProperty("dropped_image_target_kinds", dropped);
            GTEST_LOG_(INFO) << "only " << count << " image units, so these kinds are not covered by the "
                             << "combined case: " << dropped;
        }
        kinds.resize(count);

        std::string declarations;
        std::string sum;
        for (std::size_t i = 0; i < kinds.size(); ++i) {
            const std::string name = "i" + std::to_string(i);
            declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
                            kinds[i].imageType + " " + name + ";\n";
            if (!sum.empty()) sum += " + ";
            sum += LoadExpression(kinds[i], name);
        }
        const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
                                   "void main()\n{\n    uvec4 v = " + sum + ";\n    ssb.sum = v.r;\n}\n";

        const GLuint program = MakeComputeProgram(source);
        if (program == 0) return;

        // Powers of two, so the shortfall's bit pattern names exactly which kinds read zero -
        // no other subset of the values can sum to the same total. Eleven kinds at most, so the
        // largest is 1 << 10 and the sum cannot approach a uint's range.
        GLuint expected = 0;
        for (std::size_t i = 0; i < kinds.size(); ++i) {
            const GLuint value = 1u << i;
            const GLuint texture = MakeTexture(kinds[i], true, value);
            if (texture == 0) return;
            expected += value;
            glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
            ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
        }
        const GLuint ssbo = MakeResultBuffer();

        glUseProgram(program);
        for (std::size_t i = 0; i < kinds.size(); ++i) {
            glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
        }
        ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";

        glDispatchCompute(1, 1, 1);
        glMemoryBarrier(GL_ALL_BARRIER_BITS);
        EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";

        const GLuint actual = ReadResult(ssbo);
        std::string missing;
        for (std::size_t i = 0; i < kinds.size(); ++i) {
            if ((actual & (1u << i)) == 0u) {
                if (!missing.empty()) missing += ", ";
                missing += kinds[i].name;
            }
        }
        EXPECT_EQ(actual, expected)
            << "the sum over " << kinds.size()
            << " image target kinds is wrong; each kind contributes its own bit, and these read "
               "zero: "
            << (missing.empty() ? "(none - so some kind read a value it was never given)" : missing);
        glUseProgram(0);
    }

} // namespace MGITest
