// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.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
//
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
//
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
// application's capability check passes, it takes the texture-view path, and the view texture it
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
// that aliased nothing.
//
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
//   * writes through either name are visible through the other (CoherencyIsBidirectional), and
//   * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
//     entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
//     parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
//     GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
//
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
// see NoExtensionSupportIsRefusedRatherThanFaked.
//
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
// through the two names, or a value that changed after a write through the other name.

#include <array>
#include <cstdint>
#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 {
        constexpr int kSize = 64;
        // The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
        // uniform image would otherwise pass a test that only ever looked at covered texels.
        constexpr int kUncoveredTop = 16;

        constexpr const char* kQuadVertexSource = R"(#version 330 core
in vec2 aPos;
uniform vec4 uRect;   // x0, y0, x1, y1 in NDC
uniform float uDepth; // NDC z
void main() {
    vec2 p = mix(uRect.xy, uRect.zw, aPos);
    gl_Position = vec4(p, uDepth, 1.0);
}
)";

        // Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
        // draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
        // COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
        // error or clobber attachment 0.
        constexpr const char* kCoverageFragmentSource = R"(#version 330 core
layout (location = 0) out vec4 outColor;
layout (location = 1) out float outUnused;
void main() {
    outColor = vec4(1.0, 0.0, 0.0, 1.0);
    outUnused = 1.0 / 255.0;
}
)";

        // The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
        // D24S8 image through two GL texture names bound to two units in this one invocation.
        // `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
        // at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
        // it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
        constexpr const char* kShadingFragmentSource = R"(#version 330 core
uniform usampler2D uCoverage;  // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
uniform sampler2D uDepthView;  // the VIEW,   DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
out vec4 outColor;
void main() {
    uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
    float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
    outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
    gl_FragDepth = depth;
}
)";

        // Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
        // and unpacks the word back into the four bytes it was written as.
        constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
uniform usampler2D uWords;
out vec4 outColor;
void main() {
    uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
    outColor = vec4(float((word      ) & 0xFFu) / 255.0,
                    float((word >>  8) & 0xFFu) / 255.0,
                    float((word >> 16) & 0xFFu) / 255.0,
                    float((word >> 24) & 0xFFu) / 255.0);
}
)";

        constexpr const char* kSampleFragmentSource = R"(#version 330 core
uniform sampler2D uTexture;
uniform float uLod;
out vec4 outColor;
void main() {
    outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
}
)";

        std::string Describe(const Rgba8& c) {
            return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
                   std::to_string(c.a) + ")";
        }

        class TextureViewScenario : public ScenarioTest {
        protected:
            void SetUp() override {
                ScenarioTest::SetUp();
                if (!Ready()) return;
                if (!TextureViewUsable()) {
                    GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
                                 << " (GL_ARB_texture_view not advertised)";
                }
            }

            void TearDown() override {
                if (!Ready()) return;
                for (const GLuint texture : m_textures) {
                    glDeleteTextures(1, &texture);
                }
                m_textures.clear();
                for (const GLuint fbo : m_fbos) {
                    glBindFramebuffer(GL_FRAMEBUFFER, 0);
                    glDeleteFramebuffers(1, &fbo);
                }
                m_fbos.clear();
                for (const GLuint rbo : m_rbos) {
                    glDeleteRenderbuffers(1, &rbo);
                }
                m_rbos.clear();
                for (const GLuint program : m_programs) {
                    glDeleteProgram(program);
                }
                m_programs.clear();
                if (m_vao != 0) {
                    glBindVertexArray(0);
                    glDeleteVertexArrays(1, &m_vao);
                    m_vao = 0;
                }
                if (m_vbo != 0) {
                    glDeleteBuffers(1, &m_vbo);
                    m_vbo = 0;
                }
            }

            // A trivial same-format full-range view. It exercises nothing the cases below test,
            // so a backend that simply does not have the feature skips instead of failing every
            // one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
            bool TextureViewUsable() {
                GLuint storage = 0;
                glGenTextures(1, &storage);
                glBindTexture(GL_TEXTURE_2D, storage);
                glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
                glBindTexture(GL_TEXTURE_2D, 0);
                GLuint view = 0;
                glGenTextures(1, &view);
                while (glGetError() != GL_NO_ERROR) {
                }
                glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
                const bool usable = glGetError() == GL_NO_ERROR;
                glDeleteTextures(1, &view);
                glDeleteTextures(1, &storage);
                return usable;
            }

            GLuint MakeVao() {
                if (m_vao != 0) return m_vao;
                // A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
                // one buffer serves every draw here.
                static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
                                                  1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
                glGenVertexArrays(1, &m_vao);
                glBindVertexArray(m_vao);
                glGenBuffers(1, &m_vbo);
                glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
                glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
                glEnableVertexAttribArray(0);
                glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
                return m_vao;
            }

            GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
                std::string error;
                const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
                EXPECT_NE(program, 0u) << "program failed to build: " << error;
                if (program != 0) m_programs.push_back(program);
                return program;
            }

            GLuint MakeTexture() {
                GLuint texture = 0;
                glGenTextures(1, &texture);
                m_textures.push_back(texture);
                return texture;
            }

            GLuint MakeFbo() {
                GLuint fbo = 0;
                glGenFramebuffers(1, &fbo);
                m_fbos.push_back(fbo);
                return fbo;
            }

            // A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
            // here views. Levels beyond 1 stay undefined until a caller fills them.
            GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
                const GLuint texture = MakeTexture();
                glBindTexture(GL_TEXTURE_2D, texture);
                glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
                return texture;
            }

            void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
                glUseProgram(program);
                glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
                const GLint depthLocation = glGetUniformLocation(program, "uDepth");
                if (depthLocation >= 0) glUniform1f(depthLocation, depth);
                glBindVertexArray(MakeVao());
                glDrawArrays(GL_TRIANGLES, 0, 6);
            }

            // Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
            Image ReadFbo(GLuint fbo, int width, int height) {
                glBindFramebuffer(GL_FRAMEBUFFER, fbo);
                glReadBuffer(GL_COLOR_ATTACHMENT0);
                return ReadPixels(width, height);
            }

            // Every pixel of the inclusive region must match `expected` within `tolerance` per
            // channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
            // three of four vertices carrying stale data still paints a correct centre pixel.
            void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
                              const char* what) {
                int offenders = 0;
                Rgba8 firstOffender{};
                int firstX = -1;
                int firstY = -1;
                for (int y = y0; y <= y1; ++y) {
                    for (int x = x0; x <= x1; ++x) {
                        const Rgba8 actual = image.At(x, y);
                        const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
                                        std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
                                        std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
                                        std::abs(int(actual.a) - int(expected.a)) <= tolerance;
                        if (!ok) {
                            if (offenders == 0) {
                                firstOffender = actual;
                                firstX = x;
                                firstY = y;
                            }
                            ++offenders;
                        }
                    }
                }
                EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
                                        << (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
                                        << ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
                                        << Describe(expected) << " +/- " << tolerance;
            }

            std::vector<GLuint> m_textures;
            std::vector<GLuint> m_fbos;
            std::vector<GLuint> m_rbos;
            std::vector<GLuint> m_programs;
            GLuint m_vao = 0;
            GLuint m_vbo = 0;
        };

        // ------------------------------------------------------------------------------------
        // The driving case: the Better Clouds full-mode pipeline, in its real order.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
            if (!Ready() || IsSkipped()) return;

            // --- Resources.java:230-251, in order ---------------------------------------------
            const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
            const GLuint coverage = MakeTexture();
            glBindTexture(GL_TEXTURE_2D, coverage);
            glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);

            const GLuint coverageFbo = MakeFbo();
            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
            const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
            glDrawBuffers(1, drawBuffers);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
                << "the coverage framebuffer is incomplete; the mod would silently demote to its "
                   "fallback configuration here (Resources.java:187-209)";

            // The view is made from a name glGenTextures has only RESERVED - it has never been
            // bound, so glTextureView has to instantiate the texture object itself.
            const GLuint coverageDepthView = MakeTexture();
            glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
            glBindTexture(GL_TEXTURE_2D, coverageDepthView);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
            glBindTexture(GL_TEXTURE_2D, 0);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";

            // The two names must be distinguishable through the queries, or nothing below proves
            // which one produced a sample.
            GLint parentMode = 0;
            GLint viewMode = 0;
            glBindTexture(GL_TEXTURE_2D, coverage);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
            glBindTexture(GL_TEXTURE_2D, coverageDepthView);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
            glBindTexture(GL_TEXTURE_2D, 0);
            EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
            EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
                << "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
                   "the parent is precisely what a texture view exists to avoid";

            // --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
            const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
            ASSERT_NE(coverageProgram, 0u);

            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
            glViewport(0, 0, kSize, kSize);
            glEnable(GL_DEPTH_TEST);
            glDepthMask(GL_TRUE);
            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
            // Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
            glClearDepth(0.0);
            glDepthFunc(GL_GEQUAL);
            glDisable(GL_BLEND);
            glEnable(GL_STENCIL_TEST);
            glStencilMask(0xff);
            glClearStencil(0);
            // The coverage COUNT: one increment per depth-passing cloud fragment.
            glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
            glStencilFunc(GL_ALWAYS, 0xff, 0xff);
            glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            // Quad 1 covers everything above the uncovered strip, at window depth 0.25.
            const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
            DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
            // Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
            // so it both passes the depth test and increments the stencil a second time.
            DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";

            // --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
            // A different draw framebuffer, exactly as the mod does (it hands the frame back to
            // Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
            // being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
            ColorFbo destination = MakeColorFbo(kSize, kSize);
            ASSERT_NE(destination.fbo, 0u);
            GLuint destinationDepth = 0;
            glGenRenderbuffers(1, &destinationDepth);
            m_rbos.push_back(destinationDepth);
            glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
            glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
            glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));

            glViewport(0, 0, kSize, kSize);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClearDepth(0.0);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glDepthFunc(GL_GEQUAL);
            glDepthMask(GL_TRUE);
            glEnable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            // The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
            glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
            glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

            const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
            ASSERT_NE(shadingProgram, 0u);
            glUseProgram(shadingProgram);
            // Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
            // own unit assignment (Resources.java:309/311).
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, coverageDepthView);
            glActiveTexture(GL_TEXTURE3);
            glBindTexture(GL_TEXTURE_2D, coverage);
            glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
            glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
            glActiveTexture(GL_TEXTURE0);

            DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";

            const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

            // R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
            // A stub view samples (0,0,0,1), which fails the green channel of both covered
            // regions; a view that inherited the parent's stencil aspect fails them too.
            constexpr int kTolerance = 3;
            ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
                         "the uncovered strip must read coverage 0 and cleared depth 0");
            ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
                         "one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
            ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
                         kTolerance,
                         "two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
                         "through the view");

            DestroyColorFbo(destination);
        }

        // ------------------------------------------------------------------------------------
        // Storage sharing, in both directions. This is the assertion a copy-based emulation
        // fails, and the reason the no-EXT path refuses rather than emulates.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
            if (!Ready() || IsSkipped()) return;

            const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            // Render red through the PARENT's name...
            const GLuint fbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, fbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
            glViewport(0, 0, kSize, kSize);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);

            // ...and read it back through the VIEW's.
            const GLuint viewFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
                << "a texture view must be attachable like any other texture";
            Image throughView = ReadFbo(viewFbo, kSize, kSize);
            ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
                         "a write through the parent must be visible through the view");

            // Now the other direction: write green through the VIEW, read through the PARENT.
            glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
            glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            const Image throughParent = ReadFbo(fbo, kSize, kSize);
            ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
                         "a write through the view must be visible through the parent - they are one "
                         "storage, not two");
        }

        // ------------------------------------------------------------------------------------
        // Format reinterpretation within a view class (GL 4.6 core table 8.21).
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
            if (!Ready() || IsSkipped()) return;

            // GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
            // other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
            // answer a fact about the BITS rather than about the colour.
            const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
            std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
            for (std::size_t i = 0; i < texels.size(); i += 4) {
                texels[i + 0] = 0x40;
                texels[i + 1] = 0x80;
                texels[i + 2] = 0xC0;
                texels[i + 3] = 0xFF;
            }
            glBindTexture(GL_TEXTURE_2D, storage);
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
            glBindTexture(GL_TEXTURE_2D, 0);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";

            // NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
            // so a test that only asserted the view's answer could not tell "the reinterpret is
            // wrong" from "the seed never reached the GPU at all". Read the same texels through
            // the parent's own format first.
            const GLuint parentFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
            const Image seeded = ReadFbo(parentFbo, kSize, kSize);
            ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
                         "control: the storage must hold the seeded byte pattern before any view reads it");

            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
            glBindTexture(GL_TEXTURE_2D, view);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

            GLint viewFormat = 0;
            glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
            glBindTexture(GL_TEXTURE_2D, 0);
            EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";

            // A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
            // assertion below is that the view and this texture sample IDENTICALLY.
            //
            // Comparing against a reference texture rather than against a hard-coded colour is
            // deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
            // about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
            // with a raw-EGL program that reproduces the same wrong decode with NO view in play).
            // Holding both sides to the same driver factors that out completely: whatever the
            // driver makes of a usampler2D fetch, the view has to make the same thing of it, or
            // it is not delivering the storage's bits. A view that samples zero, that lands on
            // the wrong texels, or that lost its format still fails.
            constexpr std::uint32_t kExpectedWord = 0xFFC08040u;  // little-endian A,B,G,R
            const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
            std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
            glBindTexture(GL_TEXTURE_2D, reference);
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
            glBindTexture(GL_TEXTURE_2D, 0);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";

            const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
            ASSERT_NE(program, 0u);

            ColorFbo destination = MakeColorFbo(kSize, kSize);
            ASSERT_NE(destination.fbo, 0u);
            const auto decodeThrough = [&](GLuint texture) {
                glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
                glViewport(0, 0, kSize, kSize);
                glDisable(GL_DEPTH_TEST);
                glDisable(GL_STENCIL_TEST);
                glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
                glClear(GL_COLOR_BUFFER_BIT);
                glUseProgram(program);
                glActiveTexture(GL_TEXTURE0);
                glBindTexture(GL_TEXTURE_2D, texture);
                glUniform1i(glGetUniformLocation(program, "uWords"), 0);
                DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
                EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
                return ReadFbo(destination.fbo, kSize, kSize);
            };

            const Image throughReference = decodeThrough(reference);
            const Image throughView = decodeThrough(view);

            // Guard against the degenerate agreement of two black images: the reference must
            // itself carry something, or "identical" would prove nothing.
            const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
            ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
                << "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";

            std::size_t mismatches = 0;
            for (int y = 0; y < kSize; ++y) {
                for (int x = 0; x < kSize; ++x) {
                    if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
                }
            }
            EXPECT_EQ(mismatches, 0u)
                << "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
                   "holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
                << ", reference centre is " << Describe(referenceTexel);
            DestroyColorFbo(destination);
        }

        // ------------------------------------------------------------------------------------
        // Sub-ranges: one mip level of two, and one layer of an array.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
            if (!Ready() || IsSkipped()) return;

            const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
            // Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
            const GLuint seedFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
            glViewport(0, 0, kSize, kSize);
            glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
            glViewport(0, 0, kSize / 2, kSize / 2);
            glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";

            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            GLint minLevel = -1;
            GLint numLevels = -1;
            GLint immutableLevels = -1;
            glBindTexture(GL_TEXTURE_2D, view);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
            glBindTexture(GL_TEXTURE_2D, 0);
            EXPECT_EQ(minLevel, 1);
            EXPECT_EQ(numLevels, 1);
            // GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
            EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";

            // The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
            // the blue half-size image.
            const GLuint viewFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
            const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
            ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
                         "the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
        }

        TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
            if (!Ready() || IsSkipped()) return;

            constexpr int kLayers = 4;
            constexpr int kChosenLayer = 2;
            const GLuint storage = MakeTexture();
            glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
            glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
            glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            // A different colour per layer, so a view that lost its layer offset reads the wrong
            // one rather than merely reading nothing.
            const GLuint seedFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            glViewport(0, 0, kSize, kSize);
            for (int layer = 0; layer < kLayers; ++layer) {
                glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
                glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
                glClear(GL_COLOR_BUFFER_BIT);
            }
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";

            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";

            GLint minLayer = -1;
            GLint numLayers = -1;
            glBindTexture(GL_TEXTURE_2D, view);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
            glBindTexture(GL_TEXTURE_2D, 0);
            EXPECT_EQ(minLayer, kChosenLayer);
            EXPECT_EQ(numLayers, 1);

            const GLuint viewFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
            const Image sliced = ReadFbo(viewFbo, kSize, kSize);
            const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
                                 static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
            ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
                         "a single-layer 2D view of an array must address the layer it named");
        }

        // ------------------------------------------------------------------------------------
        // Writing THROUGH a layer-sliced view. The read direction is covered above; this is the
        // write direction, and it is the one that can corrupt the parent rather than merely
        // return the wrong pixels - a view whose texel path forgot its layer origin writes over
        // the parent's layer 0 while the application believes it addressed layer minLayer.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, WritingThroughALayerSlicedViewLandsOnItsOwnLayers) {
            if (!Ready() || IsSkipped()) return;

            constexpr int kLayers = 4;
            constexpr int kViewMinLayer = 2;
            const GLuint storage = MakeTexture();
            glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
            glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
            glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const auto layerFill = [](int layer) {
                return Rgba8{static_cast<std::uint8_t>(10 + layer * 20),
                             static_cast<std::uint8_t>(200 - layer * 20), 30, 255};
            };
            // Seeded by CPU sub-image rather than by rendering, deliberately: this scenario is
            // about the view's LAYER ORIGIN, and seeding through the GPU would additionally
            // depend on a CPU sub-image reaching a layer whose content the GPU wrote - which
            // DirectVulkan does not currently do even for a plain array texture (no view
            // involved), and which would make a failure here unattributable.
            const auto uploadLayer = [&](GLuint texture, int layer, Rgba8 colour) {
                std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
                for (std::size_t i = 0; i < texels.size(); i += 4) {
                    texels[i + 0] = colour.r;
                    texels[i + 1] = colour.g;
                    texels[i + 2] = colour.b;
                    texels[i + 3] = colour.a;
                }
                glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
                glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
                glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                                texels.data());
                glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
            };
            for (int layer = 0; layer < kLayers; ++layer) {
                uploadLayer(storage, layer, layerFill(layer));
            }
            const GLuint fbo = MakeFbo();
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the layers raised an error";

            // A two-layer window starting at layer 2, so a lost offset lands on layer 0 - which
            // the assertions below would see as an untouched layer that moved.
            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, kViewMinLayer, 2);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            // Write the view's OWN layer 0, i.e. the storage's layer 2.
            constexpr Rgba8 kPainted{255, 0, 255, 255};
            std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
            for (std::size_t i = 0; i < texels.size(); i += 4) {
                texels[i + 0] = kPainted.r;
                texels[i + 1] = kPainted.g;
                texels[i + 2] = kPainted.b;
                texels[i + 3] = kPainted.a;
            }
            glBindTexture(GL_TEXTURE_2D_ARRAY, view);
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                            texels.data());
            glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "writing through the view raised an error";

            // POSITIVE CONTROL, through the parent's own name and into a layer outside the view's
            // window. It makes the assertions below able to tell "the view lost its layer origin"
            // from "a CPU sub-image into this array does not reach the GPU at all", which is a
            // different question and not one a texture view can answer.
            constexpr Rgba8 kControl{0, 0, 255, 255};
            std::vector<std::uint8_t> controlTexels(texels.size());
            for (std::size_t i = 0; i < controlTexels.size(); i += 4) {
                controlTexels[i + 0] = kControl.r;
                controlTexels[i + 1] = kControl.g;
                controlTexels[i + 2] = kControl.b;
                controlTexels[i + 3] = kControl.a;
            }
            glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                            controlTexels.data());
            glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the control write raised an error";

            // Read every layer of the PARENT back: only the one the view's layer 0 maps to may
            // have changed.
            for (int layer = 0; layer < kLayers; ++layer) {
                glBindFramebuffer(GL_FRAMEBUFFER, fbo);
                glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
                glReadBuffer(GL_COLOR_ATTACHMENT0);
                const Image image = ReadPixels(kSize, kSize);
                Rgba8 expected = layerFill(layer);
                const char* what = "a layer outside the view's window must not have been written";
                if (layer == kViewMinLayer) {
                    expected = kPainted;
                    what = "the view's layer 0 must be the storage layer it named";
                } else if (layer == 1) {
                    expected = kControl;
                    what = "control: a sub-image written through the PARENT must reach its layer";
                }
                ExpectRegion(image, 0, kSize - 1, 0, kSize - 1, expected, 2, what);
            }
        }

        // ------------------------------------------------------------------------------------
        // Views of views compose; the composed view still reaches the ROOT storage.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
            if (!Ready() || IsSkipped()) return;

            constexpr int kLevels = 3;
            const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
            const GLuint seedFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            for (int level = 0; level < kLevels; ++level) {
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
                glViewport(0, 0, kSize >> level, kSize >> level);
                glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
                glClear(GL_COLOR_BUFFER_BIT);
            }
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            // First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
            // root's level 2. GL 4.6 core 8.18 makes the offsets add.
            const GLuint firstView = MakeTexture();
            glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            const GLuint secondView = MakeTexture();
            glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";

            GLint minLevel = -1;
            GLint numLevels = -1;
            glBindTexture(GL_TEXTURE_2D, secondView);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
            glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
            glBindTexture(GL_TEXTURE_2D, 0);
            EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
            EXPECT_EQ(numLevels, 1);

            const GLuint viewFbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
            ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
            const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
            ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
                         "the composed view must land on the root's level 2");
        }

        // ------------------------------------------------------------------------------------
        // GL name-deletion semantics: the storage outlives the original's NAME.
        // ------------------------------------------------------------------------------------
        TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
            if (!Ready() || IsSkipped()) return;

            GLuint storage = 0;
            glGenTextures(1, &storage);
            glBindTexture(GL_TEXTURE_2D, storage);
            glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

            const GLuint view = MakeTexture();
            glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const GLuint fbo = MakeFbo();
            glBindFramebuffer(GL_FRAMEBUFFER, fbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
            glViewport(0, 0, kSize, kSize);
            glDisable(GL_DEPTH_TEST);
            glDisable(GL_STENCIL_TEST);
            glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);

            // The NAME goes; the storage may not, because a view still references it
            // (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
            glDeleteTextures(1, &storage);
            EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            // Sample the view through a shader, so the answer comes from a live descriptor rather
            // than from an attachment the frontend might have kept alive by other means.
            ColorFbo destination = MakeColorFbo(kSize, kSize);
            ASSERT_NE(destination.fbo, 0u);
            glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
            glViewport(0, 0, kSize, kSize);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);

            const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
            ASSERT_NE(program, 0u);
            glUseProgram(program);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, view);
            glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
            glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
            DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
            ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
                         "the view must still reach its storage after the original's name was deleted");
            DestroyColorFbo(destination);
        }
    } // namespace
} // namespace MGITest
