// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.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 - glReadPixels OF DEPTH AND STENCIL FROM THE DEFAULT FRAMEBUFFER.
//
// DirectVulkan's depth/stencil readback used to decline the default framebuffer outright
// (`ReadDepthStencilPixels` returned at its first line) because that framebuffer's depth and
// stencil "attachments" are placeholder texture objects backing no image - the real one is the
// swapchain's depth/stencil twin. Declining meant the call raised no GL error and wrote NOTHING,
// so the caller kept whatever its buffer already held.
//
// That silence is what the framebuffer_blit family trips over. Every one of its cases begins by
// clearing the default framebuffer's depth and stencil and reading them straight back as a
// sanity check, into a local pre-initialised to 0.2 (depth) and 50 (stencil); an untouched
// buffer therefore reports "expected DEPTH[0.25] but got DEPTH[0.2]" and "expected STENCIL[1] but
// got STENCIL[50]" - the exact strings in the 15 Magma failures - long before any blit happens.
// A test that only checked "no GL error" would pass against the broken path, so every case here
// poisons its destination with a value the correct answer cannot be.
//
// The orientation case is the second half. This renderer stores the default framebuffer
// display-side-up and converts GL rects on their way in, so the depth copy needs the same rect
// mapping and row re-ordering the colour readback got in the M-1 fix; without them a
// vertically-varying depth buffer reads back mirrored, which no full-extent uniform-value test
// can see.
//
// Depth/stencil readback through a USER framebuffer already worked and is asserted here too, as
// the built-in control: it shares ReadDepthStencilImageToClient with the default-framebuffer
// path, so it is what says a failure is about the default framebuffer specifically.

#include <cmath>
#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 {

        // Values no correct read can produce, so "the backend wrote nothing" fails loudly instead
        // of passing on whatever happened to be in the variable. These are the CTS's own poison
        // values, which is why its logs report exactly them.
        constexpr float kDepthPoison = 0.2f;
        constexpr int kStencilPoison = 50;

        class DepthStencilReadbackScenario : public ScenarioTest {
        protected:
            // Both backends now answer these reads. DirectGLES has no native ES path for
            // either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on
            // both the Adreno device and Mesa's ES), so it stages the attachment into a
            // scratch depth texture and samples it into a colour target; the assertions below
            // are the same either way, which is the point.
            bool BackendReadsDepthStencil() const { return true; }

            float ReadDepthAt(int x, int y) const {
                float depth = kDepthPoison;
                glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
                return depth;
            }

            int ReadStencilAt(int x, int y) const {
                int stencil = kStencilPoison;
                glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
                return stencil;
            }
        };

        // A depth buffer whose value depends on the row: bottom half `bottom`, top half `top`.
        // Built with a scissored clear rather than a draw so the test stays independent of
        // depth-test and shader behaviour.
        void ClearDepthInBands(int width, int height, float bottom, float top) {
            glEnable(GL_SCISSOR_TEST);
            glScissor(0, 0, width, height / 2);
            glClearDepth(bottom);
            glClear(GL_DEPTH_BUFFER_BIT);
            glScissor(0, height / 2, width, height - height / 2);
            glClearDepth(top);
            glClear(GL_DEPTH_BUFFER_BIT);
            glDisable(GL_SCISSOR_TEST);
        }

    } // namespace

    TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthClearIsVisibleToReadPixels) {
        if (!Ready()) return;
        if (!BackendReadsDepthStencil()) {
            GTEST_SKIP() << "backend " << Gl().BackendName()
                         << " has no depth readback path (ES lacks GL_NV_read_depth); see the packed_depth_stencil "
                            "cluster";
        }
        HeadlessGL& gl = Gl();
        const int width = gl.Width();
        const int height = gl.Height();

        BindDefaultFramebuffer();
        glViewport(0, 0, width, height);
        glDisable(GL_SCISSOR_TEST);
        glDepthMask(GL_TRUE);
        glClearDepth(0.25);
        glClear(GL_DEPTH_BUFFER_BIT);

        const float centre = ReadDepthAt(width / 2, height / 2);
        EXPECT_EQ(FirstGLError(), 0u);
        EXPECT_NEAR(centre, 0.25f, 1.0f / 4096.0f)
            << "glReadPixels(GL_DEPTH_COMPONENT) of the default framebuffer returned " << centre
            << (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");

        gl.EndFrame();
    }

    TEST_F(DepthStencilReadbackScenario, DefaultFramebufferStencilClearIsVisibleToReadPixels) {
        if (!Ready()) return;
        if (!BackendReadsDepthStencil()) {
            GTEST_SKIP() << "backend " << Gl().BackendName()
                         << " has no stencil readback path (ES lacks GL_NV_read_stencil); see the "
                            "packed_depth_stencil cluster";
        }
        HeadlessGL& gl = Gl();
        const int width = gl.Width();
        const int height = gl.Height();

        BindDefaultFramebuffer();
        glViewport(0, 0, width, height);
        glDisable(GL_SCISSOR_TEST);
        glStencilMask(0xFFu);
        glClearStencil(3);
        glClear(GL_STENCIL_BUFFER_BIT);

        const int centre = ReadStencilAt(width / 2, height / 2);
        EXPECT_EQ(FirstGLError(), 0u);
        EXPECT_EQ(centre, 3) << "glReadPixels(GL_STENCIL_INDEX) of the default framebuffer returned " << centre
                             << (centre == kStencilPoison ? " - the destination was never written at all" : "");

        gl.EndFrame();
    }

    // The orientation half: a depth buffer that varies with the row must read back in GL's
    // bottom-up order. A full-extent uniform clear is a fixed point of the flip, so only a banded
    // buffer can tell the two apart.
    TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthReadbackKeepsTheGLRowOrder) {
        if (!Ready()) return;
        if (!BackendReadsDepthStencil()) {
            GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
        }
        HeadlessGL& gl = Gl();
        const int width = gl.Width();
        const int height = gl.Height();
        ASSERT_GE(height, 8);

        BindDefaultFramebuffer();
        glViewport(0, 0, width, height);
        glDepthMask(GL_TRUE);
        ClearDepthInBands(width, height, /*bottom=*/0.25f, /*top=*/0.75f);
        EXPECT_EQ(FirstGLError(), 0u);

        const float bottom = ReadDepthAt(width / 2, height / 4);
        const float top = ReadDepthAt(width / 2, height - 1 - height / 4);
        EXPECT_EQ(FirstGLError(), 0u);
        EXPECT_NEAR(bottom, 0.25f, 1.0f / 4096.0f)
            << "GL row " << (height / 4) << " is in the bottom band and was cleared to 0.25, but read back " << bottom
            << " (0.75 there means the readback is upside down)";
        EXPECT_NEAR(top, 0.75f, 1.0f / 4096.0f)
            << "GL row " << (height - 1 - height / 4) << " is in the top band and was cleared to 0.75, but read back "
            << top << " (0.25 there means the readback is upside down)";

        gl.EndFrame();
    }

    // A depth blit INTO the default framebuffer has to convert its rect out of GL's bottom-origin
    // space, exactly as the colour blit does. The colour path had that conversion and the
    // depth path did not, so a scissored depth blit landed in the mirrored band - which is the
    // whole of KHR-GL*.framebuffer_blit.scissor_blit once the readback above works well enough to
    // see it (before that the test died on the poison values and never reached the blit).
    TEST_F(DepthStencilReadbackScenario, AScissoredDepthBlitIntoTheDefaultFramebufferLandsInTheScissorBox) {
        if (!Ready()) return;
        if (!BackendReadsDepthStencil()) {
            GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
        }
        HeadlessGL& gl = Gl();
        const int width = gl.Width();
        const int height = gl.Height();
        ASSERT_GE(width, 8);
        ASSERT_GE(height, 8);

        // Source: a user framebuffer whose depth is uniformly 0.75.
        GLuint fbo = 0, colorTex = 0, depthTex = 0;
        glGenFramebuffers(1, &fbo);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glGenTextures(1, &colorTex);
        glBindTexture(GL_TEXTURE_2D, colorTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
        glGenTextures(1, &depthTex);
        glBindTexture(GL_TEXTURE_2D, depthTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
                     GL_UNSIGNED_INT_24_8, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
        ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
        glViewport(0, 0, width, height);
        glDisable(GL_SCISSOR_TEST);
        glDepthMask(GL_TRUE);
        glClearDepth(0.75);
        glClear(GL_DEPTH_BUFFER_BIT);

        // Destination: the default framebuffer, depth 0 everywhere.
        BindDefaultFramebuffer();
        glViewport(0, 0, width, height);
        glClearDepth(0.0);
        glClear(GL_DEPTH_BUFFER_BIT);

        // Blit the whole rect, but scissored to the BOTTOM-LEFT quadrant in GL coordinates.
        glEnable(GL_SCISSOR_TEST);
        glScissor(0, 0, width / 2, height / 2);
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
        glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
        glDisable(GL_SCISSOR_TEST);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        EXPECT_EQ(FirstGLError(), 0u);

        const float inside = ReadDepthAt(width / 4, height / 4);
        const float above = ReadDepthAt(width / 4, height - 1 - height / 4);
        EXPECT_EQ(FirstGLError(), 0u);
        EXPECT_NEAR(inside, 0.75f, 1.0f / 4096.0f)
            << "GL (" << (width / 4) << ", " << (height / 4) << ") is inside the scissor box and should hold the "
            << "blitted 0.75, but read back " << inside;
        EXPECT_NEAR(above, 0.0f, 1.0f / 4096.0f)
            << "GL (" << (width / 4) << ", " << (height - 1 - height / 4)
            << ") is ABOVE the scissor box and must still hold the cleared 0.0, but read back " << above
            << " (0.75 there means the depth blit landed in the mirrored band)";

        glDeleteTextures(1, &depthTex);
        glDeleteTextures(1, &colorTex);
        glDeleteFramebuffers(1, &fbo);
        gl.EndFrame();
    }

    // The control: the same read against a user framebuffer, which never went through the
    // declined path. It is what makes a failure above specific to the default framebuffer.
    TEST_F(DepthStencilReadbackScenario, UserFramebufferDepthClearIsVisibleToReadPixels) {
        if (!Ready()) return;
        if (!BackendReadsDepthStencil()) {
            GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
        }
        HeadlessGL& gl = Gl();
        const int width = 64;
        const int height = 48;

        GLuint fbo = 0, colorTex = 0, depthTex = 0;
        glGenFramebuffers(1, &fbo);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glGenTextures(1, &colorTex);
        glBindTexture(GL_TEXTURE_2D, colorTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
        glGenTextures(1, &depthTex);
        glBindTexture(GL_TEXTURE_2D, depthTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
                     GL_UNSIGNED_INT_24_8, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
        ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
        ASSERT_EQ(FirstGLError(), 0u);

        glViewport(0, 0, width, height);
        glDisable(GL_SCISSOR_TEST);
        glDepthMask(GL_TRUE);
        glStencilMask(0xFFu);
        glClearDepth(0.5);
        glClearStencil(7);
        glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        const float depth = ReadDepthAt(width / 2, height / 2);
        const int stencil = ReadStencilAt(width / 2, height / 2);
        EXPECT_EQ(FirstGLError(), 0u);
        EXPECT_NEAR(depth, 0.5f, 1.0f / 4096.0f) << "user-framebuffer depth readback returned " << depth;
        EXPECT_EQ(stencil, 7) << "user-framebuffer stencil readback returned " << stencil;

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glDeleteTextures(1, &depthTex);
        glDeleteTextures(1, &colorTex);
        glDeleteFramebuffers(1, &fbo);
        gl.EndFrame();
    }
} // namespace MGITest
