// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.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 - A BUFFER TEXTURE IS SAMPLED FROM THE VERTEX STAGE, AND TRACKS ITS BUFFER.
//
// Buffer textures are core in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
// application may build geometry out of one without asking whether the host can. Minecraft
// 26.3 does exactly that: its cloud layer has no vertex attributes at all, only gl_VertexID
// and texelFetch on a GL_R8I buffer texture. Nothing covered that path end to end on either
// backend - the frontend unit tests stop at glTexBuffer's state, and no scenario ever drew
// with the result - which is how DirectGLES came to emit `#extension GL_EXT_texture_buffer :
// require` unconditionally, compile nothing on a host without the extension, and lose the
// whole cloud layer with no diagnostic anywhere.
//
// Two claims, in the order they can break:
//   1. a vertex-stage texelFetch on an R8I buffer texture reads the byte the application put
//      in the buffer (the shape of the real workload: no attributes, index from gl_VertexID);
//   2. a later glBufferSubData is visible to the next draw WITHOUT re-specifying the texture.
//      glTexBuffer attaches storage, it does not copy: the texture is a live view of the
//      buffer, so a backend that only refreshes the view when the texture's own state changes
//      must still show the new bytes. DirectGLES' respecify gate is keyed on the texture info
//      and deliberately does not include the buffer's contents, so this is the assertion that
//      says that is safe rather than merely untested.
//
// NOTE ON A HOST WITHOUT BUFFER TEXTURES: this scenario is expected to FAIL there, and that is
// the honest outcome - MobileGL keeps advertising GL_MAX_TEXTURE_BUFFER_SIZE (an OpenGL 4.x
// context may not answer 0), so there is no capability an application, or this test, could
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.

#include <cstdint>
#include <cstring>
#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 {

        // No vertex attributes: the quad's corners come from gl_VertexID, exactly like the
        // workload this exists for. The texel is fetched in the VERTEX stage - the stage where
        // buffer-texture support is scarcest across ES drivers - and carried flat so every
        // fragment of the quad reports the same byte and the readback is exact.
        constexpr const char* kVS = R"(#version 330 core
uniform isamplerBuffer uFaces;
flat out int vFace;
void main() {
    vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
                       (gl_VertexID & 2) == 0 ? -1.0 : 1.0);
    vFace = texelFetch(uFaces, 0).r;
    gl_Position = vec4(corner, 0.0, 1.0);
}
)";

        // 1/255 steps survive an RGBA8 round trip exactly, so the readback byte IS the value
        // the vertex shader fetched.
        constexpr const char* kFS = R"(#version 330 core
flat in int vFace;
out vec4 o_color;
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
)";

        // A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
        // another, so a single dispatch proves the read direction (which already worked) and
        // the write direction (which is what this exists for) apart from each other.
        constexpr const char* kImageBufferCS = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, rgba8) uniform imageBuffer uImage;
void main() {
    vec4 read = imageLoad(uImage, 1);
    imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
    imageStore(uImage, 2, read);
}
)";

        class BufferTextureScenario : public ScenarioTest {
        protected:
            bool ComputeImagesAreUsable() const {
                GLint maxImageUnits = 0;
                GLint maxComputeImageUniforms = 0;
                glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
                glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
                while (glGetError() != GL_NO_ERROR) {
                }
                return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
            }

            unsigned int MakeComputeProgram(const char* source) {
                const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
                glShaderSource(shader, 1, &source, 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;
                    glDeleteShader(shader);
                    return 0;
                }
                const GLuint program = glCreateProgram();
                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;
                    glDeleteProgram(program);
                    return 0;
                }
                return program;
            }
        };

        // Draws the full-viewport quad and returns the red byte every fragment was painted with,
        // or -1 if the quad did not come out uniform (which would mean the flat varying, not the
        // fetch, is what this test is measuring).
        int PaintedValue(unsigned int program, int width, int height) {
            ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
            GLuint vao = 0;
            glGenVertexArrays(1, &vao);
            glBindVertexArray(vao);
            glUseProgram(program);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glBindVertexArray(0);
            glDeleteVertexArrays(1, &vao);

            const Image image = ReadPixels(width, height);
            if (image.Empty()) {
                return -1;
            }
            const int first = image.At(0, 0).r;
            for (int y = 0; y < image.Height(); ++y) {
                for (int x = 0; x < image.Width(); ++x) {
                    if (image.At(x, y).r != first) {
                        return -1;
                    }
                }
            }
            return first;
        }

    } // namespace

    TEST_F(BufferTextureScenario, VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates) {
        if (!Ready()) return;
        HeadlessGL& gl = Gl();

        std::string error;
        const unsigned int program = CompileProgram(kVS, kFS, &error);
        ASSERT_NE(program, 0u) << error;

        // GL_R8I is the format the real workload uses. Signed, so the values stay well inside
        // [0, 127] to keep the readback arithmetic honest.
        constexpr signed char kInitial = 37;
        constexpr signed char kUpdated = 91;
        std::vector<signed char> texels(64, 0);
        texels[0] = kInitial;

        // The harness shares one context across every scenario in the process, so an error left
        // by an earlier one would surface below as "glTexBuffer was refused".
        FirstGLError();

        GLuint buffer = 0;
        glGenBuffers(1, &buffer);
        glBindBuffer(GL_TEXTURE_BUFFER, buffer);
        glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size()), texels.data(),
                     GL_DYNAMIC_DRAW);

        GLuint texture = 0;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_BUFFER, texture);
        glTexBuffer(GL_TEXTURE_BUFFER, GL_R8I, buffer);
        ASSERT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_R8I) was refused";

        ColorFbo target = MakeColorFbo(64, 64);
        ASSERT_NE(target.fbo, 0u) << "could not create the render target";
        BindFbo(target);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_BUFFER, texture);
        glUseProgram(program);
        const GLint location = glGetUniformLocation(program, "uFaces");
        ASSERT_NE(location, -1) << "the buffer sampler was optimized away or never reflected";
        glUniform1i(location, 0);

        EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kInitial))
            << "a vertex-stage texelFetch on an R8I buffer texture did not read the byte the "
               "application stored (a uniform -1 here means the quad was not uniform at all)";

        // The texture is a VIEW of the buffer: no glTexBuffer call follows, and none should be
        // needed for the new bytes to be visible.
        glBindBuffer(GL_TEXTURE_BUFFER, buffer);
        glBufferSubData(GL_TEXTURE_BUFFER, 0, 1, &kUpdated);
        ASSERT_EQ(FirstGLError(), 0u) << "glBufferSubData on the texture's buffer was refused";

        EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kUpdated))
            << "the buffer texture kept showing the old contents after glBufferSubData; the "
               "texture must track its buffer without being re-specified";

        BindDefaultFramebuffer();
        DestroyColorFbo(target);
        glUseProgram(0);
        glDeleteProgram(program);
        glDeleteTextures(1, &texture);
        glDeleteBuffers(1, &buffer);
        glViewport(0, 0, gl.Width(), gl.Height());
        EXPECT_EQ(FirstGLError(), 0u);
    }

    // A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
    // land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
    // and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
    // and the shadow is refreshed on the next read; a buffer reached through an image unit is
    // the same write through a different binding, and Espryt used to flag only the first, so
    // an imageStore into a buffer texture was invisible to every CPU read that followed it -
    // silently, with the correct value sitting in the driver's buffer the whole time.
    //
    // The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
    // failure here cannot be blamed on the image binding not working at all.
    TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
        if (!Ready()) return;
        if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";

        constexpr GLuint kRed = 0x000000ffu;   // RGBA8 little-endian: r = 255
        constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
        constexpr int kTexels = 16;

        FirstGLError();

        const unsigned int program = MakeComputeProgram(kImageBufferCS);
        ASSERT_NE(program, 0u);

        const std::vector<GLuint> texels(kTexels, kRed);
        GLuint buffer = 0;
        glGenBuffers(1, &buffer);
        glBindBuffer(GL_TEXTURE_BUFFER, buffer);
        glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
                     GL_DYNAMIC_COPY);

        GLuint texture = 0;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_BUFFER, texture);
        glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
        EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";

        glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
        EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";

        glUseProgram(program);
        glDispatchCompute(1, 1, 1);
        glMemoryBarrier(GL_ALL_BARRIER_BITS);

        // Both CPU read paths, because they are two entry points onto the same refresh and a
        // fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
        // ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
        // context across every scenario in the process, and a leaked buffer or image binding
        // here would surface as a failure somewhere else entirely.
        std::vector<GLuint> readBack(kTexels, 0u);
        glBindBuffer(GL_TEXTURE_BUFFER, buffer);
        glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
                           readBack.data());
        EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
        EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";

        const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
        EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
        if (mapped != nullptr) {
            GLuint mappedTexel0 = 0;
            std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
            EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
            glUnmapBuffer(GL_TEXTURE_BUFFER);
        }

        glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
        glBindBuffer(GL_TEXTURE_BUFFER, 0);
        glBindTexture(GL_TEXTURE_BUFFER, 0);
        glUseProgram(0);
        glDeleteProgram(program);
        glDeleteTextures(1, &texture);
        glDeleteBuffers(1, &buffer);
        EXPECT_EQ(FirstGLError(), 0u);
    }

    // glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
    // fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
    // GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
    // KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
    // the case before any value is compared.
    //
    // The two halves report DIFFERENT units and only one of them is clamped, which is the thing
    // easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
    // BUFFER_SIZE is the range in basic machine units exactly as it was given.
    TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
        if (!Ready()) return;
        FirstGLError();

        GLint offsetAlignment = 1;
        glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
        if (offsetAlignment < 1) offsetAlignment = 1;
        GLint maxTexels = 0;
        glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
        ASSERT_EQ(FirstGLError(), 0u);
        ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";

        constexpr GLint kTexelBytes = 4; // GL_RGBA8
        const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
        const GLsizeiptr rangeBytes = 32 * kTexelBytes;
        // Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
        // than out of the texture's window would be caught.
        const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;

        const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
        GLuint buffer = 0;
        glGenBuffers(1, &buffer);
        glBindBuffer(GL_TEXTURE_BUFFER, buffer);
        glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);

        GLuint texture = 0;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_BUFFER, texture);
        glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
        ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";

        const auto levelQuery = [](GLenum pname) {
            GLint value = -1;
            glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
            return value;
        };
        const auto levelQueryF = [](GLenum pname) {
            GLfloat value = -1.0f;
            glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
            return value;
        };

        EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
            << "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
        EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
        EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
        EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
            << "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
        EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
        EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
        EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
            << "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";

        // The float getter is a separate switch and has drifted from the integer one before.
        EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
        EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
        EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
        EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";

        // The whole-buffer form follows the buffer's current size instead of freezing a window.
        glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
        EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
        EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
        EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
        EXPECT_EQ(FirstGLError(), 0u);

        // Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
        // the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
        GLuint plainTexture = 0;
        glGenTextures(1, &plainTexture);
        glBindTexture(GL_TEXTURE_2D, plainTexture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        EXPECT_EQ(FirstGLError(), 0u);
        GLint unused = -1;
        glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
        EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));

        glBindTexture(GL_TEXTURE_2D, 0);
        glBindTexture(GL_TEXTURE_BUFFER, 0);
        glBindBuffer(GL_TEXTURE_BUFFER, 0);
        glDeleteTextures(1, &plainTexture);
        glDeleteTextures(1, &texture);
        glDeleteBuffers(1, &buffer);
        EXPECT_EQ(FirstGLError(), 0u);
    }

} // namespace MGITest
