// MobileGL - MobileGL/MG_State/GLState/Core.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

#include "Core.h"
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
#include "MG_State/EGLState/Core.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <Config.h>

namespace MobileGL::MG_State {
    void Init() {
        MGLOG_D("Initializing MobileGL State...");
        pGLContext = MakeUnique<GLState::GLContext>();
        pEGLContext = MakeUnique<EGLState::EGLContext>();
    }

    Bool IsRelaxedSemanticsActive() {
        return MG_Config::Features.RelaxedSemantics ||
               !(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
    }

    namespace GLState {
        const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GLContext::GetCompileEnv() {
            const void* backend = static_cast<const void*>(MG_Backend::pActiveBackendObject.get());
            if (!m_compileEnv || m_compileEnvBackend != backend) {
                // First use, or the backend was swapped underneath us. Re-capturing rolls the
                // fingerprint, so every P0b preprocess memo computed against the old backend's
                // limits becomes structurally unreachable instead of silently reusable.
                m_compileEnv = MG_Util::ShaderTranspiler::CaptureCompileEnv();
                m_compileEnvBackend = backend;
            }
            return m_compileEnv;
        }

        void GLContext::InvalidateCompileEnv() {
            m_compileEnv.reset();
            m_compileEnvBackend = nullptr;
        }

        // Error
        void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
            // Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
            // A compile or link body that needs to raise an error must append to its node's
            // JobDiagnostics and let the join replay it here (see the P1 design section 6);
            // reaching this from a worker would corrupt the sticky-flag set that
            // glGetError's ordering depends on.
            MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
                            "GLContext::RecordError() called from a shader-compile pool thread");
            m_errorState.RecordError(code, Move(info));
        }

        Bool GLContext::HasGLError() const {
            return m_errorState.HasGLError();
        }

        Optional<const Error*> GLContext::PeekGLError() const {
            return m_errorState.PeekGLError();
        }

        Optional<UniquePtr<Error>> GLContext::PopGLError() {
            return Move(m_errorState.PopGLError());
        }

        Bool GLContext::HasNonGLError() const {
            return m_errorState.HasNonGLError();
        }

        Optional<const Error*> GLContext::PeekNonGLError() const {
            return m_errorState.PeekNonGLError();
        }

        Optional<UniquePtr<Error>> GLContext::PopNonGLError() {
            return Move(m_errorState.PopNonGLError());
        }

        void GLContext::ClearErrors() {
            m_errorState.Clear();
        }

        // Buffer
        void GLContext::GenBufferNames(Uint number, Vector<Uint>& buffers) {
            m_bufferState.GenerateNames(number, buffers);
        }

        const SharedPtr<BufferObject>& GLContext::GetBufferObject(Uint index) {
            return m_bufferState.GetBufferObject(index);
        }

        BindingSlot<BufferObject>& GLContext::GetBufferBindingSlot(BufferTarget target) {
            if (target == BufferTarget::Index) {
                const auto& vao = m_vertexArrayState.GetBoundVertexArray();
                MOBILEGL_ASSERT(vao != nullptr, "No VAO is currently bound when accessing index buffer binding slot.");
                return vao->GetIndexBufferBindingSlot();
            }

            return m_bufferState.GetBindingSlot(target);
        }

        BindingSlotRange1D<BufferObject>& GLContext::GetBufferBindingPoint(BufferTarget target, Uint index) {
            return m_bufferState.GetBindingPoint(target, index);
        }

        const SharedPtr<BufferObject>& GLContext::CreateBufferObject(Uint index) {
            return m_bufferState.CreateBufferObject(index);
        }

        void GLContext::MarkBufferObjectForDeletion(Uint index) {
            if (ValidateBufferObject(index)) {
                // GL semantics: deleting a buffer detaches it only from the CURRENT
                // context's bindings, including the currently bound VAO's attachment
                // points; attachments in other VAOs must survive (the shared_ptr keeps
                // the data object alive, matching the spec's deferred deletion). The
                // previous every-VAO scan was wrong per spec and O(VAOs) per delete —
                // with one VAO per chunk section, vanilla's steady buffer churn made it
                // dominate the render thread and FPS decay over session time.
                auto bufferObject = m_bufferState.GetBufferObject(index);
                const auto& vao = m_vertexArrayState.GetBoundVertexArray();
                if (vao != nullptr) {
                    if (vao->GetIndexBufferBindingSlot().GetBoundObject() == bufferObject) {
                        vao->GetIndexBufferBindingSlot().Bind(nullptr);
                    }
                    for (SizeT j = 0; j < VertexArrayObject::MAX_VERTEX_ATTRIBS; ++j) {
                        if (vao->GetAttribute(j).Buffer == bufferObject) {
                            vao->BindAttributeBuffer(j, nullptr);
                        }
                    }
                }
            }

            m_bufferState.MarkBufferObjectForDeletion(index);
        }

        Bool GLContext::ValidateBufferName(Uint index) const {
            return m_bufferState.ValidateName(index);
        }

        Bool GLContext::ValidateBufferObject(Uint index) const {
            return m_bufferState.ValidateBufferObject(index);
        }

        // VertexArray
        void GLContext::GenVertexArrayNames(Uint number, Vector<Uint>& vertexArrays) {
            m_vertexArrayState.GenerateNames(number, vertexArrays);
        }

        const SharedPtr<VertexArrayObject>& GLContext::GetVertexArrayObject(Uint index) {
            return m_vertexArrayState.GetVertexArrayObject(index);
        }

        void GLContext::BindVertexArray(Uint index) {
            m_vertexArrayState.Bind(index);
        }

        const SharedPtr<VertexArrayObject>& GLContext::CreateVertexArrayObject(Uint index) {
            return m_vertexArrayState.CreateVertexArrayObject(index);
        }

        void GLContext::MarkVertexArrayForDeletion(Uint index) {
            m_vertexArrayState.MarkVertexArrayForDeletion(index);
        }

        Bool GLContext::ValidateVertexArrayName(Uint index) const {
            return m_vertexArrayState.ValidateName(index);
        }

        Bool GLContext::ValidateVertexArrayObject(Uint index) const {
            return m_vertexArrayState.ValidateVertexArrayObject(index);
        }

        const SharedPtr<VertexArrayObject>& GLContext::GetBoundVertexArray() {
            return m_vertexArrayState.GetBoundVertexArray();
        }

        VertexAttribTypeInfo ClassifyVertexAttribType(GLenum glType) {
            switch (glType) {
            case GL_FLOAT: return {VertexAttribBaseType::Float, 1};
            case GL_FLOAT_VEC2: return {VertexAttribBaseType::Float, 2};
            case GL_FLOAT_VEC3: return {VertexAttribBaseType::Float, 3};
            case GL_FLOAT_VEC4: return {VertexAttribBaseType::Float, 4};
            case GL_INT: return {VertexAttribBaseType::Int, 1};
            case GL_INT_VEC2: return {VertexAttribBaseType::Int, 2};
            case GL_INT_VEC3: return {VertexAttribBaseType::Int, 3};
            case GL_INT_VEC4: return {VertexAttribBaseType::Int, 4};
            case GL_UNSIGNED_INT: return {VertexAttribBaseType::Uint, 1};
            case GL_UNSIGNED_INT_VEC2: return {VertexAttribBaseType::Uint, 2};
            case GL_UNSIGNED_INT_VEC3: return {VertexAttribBaseType::Uint, 3};
            case GL_UNSIGNED_INT_VEC4: return {VertexAttribBaseType::Uint, 4};
            default: return {};
            }
        }

        // The three accessors below are reachable from backend draw paths with a location taken from
        // shader reflection, so the bound must be enforced at runtime rather than by MOBILEGL_ASSERT
        // (which expands to nothing outside debug builds).
        void GLContext::SetCurrentVertexAttributeFloat(Uint index, const Array<Float, 4>& value) {
            if (index >= m_currentVertexAttributes.size()) {
                MGLOG_E_ONCE("SetCurrentVertexAttributeFloat: index %u is out of range", index);
                return;
            }

            auto& current = m_currentVertexAttributes[index];
            current.floatValue = value;
            for (SizeT component = 0; component < value.size(); ++component) {
                current.intValue[component] = static_cast<Int32>(value[component]);
                current.uintValue[component] = static_cast<Uint32>(value[component]);
            }
        }

        void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
            if (index >= m_currentVertexAttributes.size()) {
                MGLOG_E_ONCE("SetCurrentVertexAttributeInt: index %u is out of range", index);
                return;
            }

            auto& current = m_currentVertexAttributes[index];
            current.intValue = value;
            for (SizeT component = 0; component < value.size(); ++component) {
                current.floatValue[component] = static_cast<Float>(value[component]);
                current.uintValue[component] = static_cast<Uint32>(value[component]);
            }
        }

        void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
            if (index >= m_currentVertexAttributes.size()) {
                MGLOG_E_ONCE("SetCurrentVertexAttributeUint: index %u is out of range", index);
                return;
            }

            auto& current = m_currentVertexAttributes[index];
            current.uintValue = value;
            for (SizeT component = 0; component < value.size(); ++component) {
                current.floatValue[component] = static_cast<Float>(value[component]);
                current.intValue[component] = static_cast<Int32>(value[component]);
            }
        }

        const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
            static const CurrentVertexAttributeValue defaultValue{};
            if (index >= m_currentVertexAttributes.size()) {
                MGLOG_E_ONCE("GetCurrentVertexAttribute: index %u is out of range", index);
                return defaultValue;
            }
            return m_currentVertexAttributes[index];
        }

        // Texture
        void GLContext::GenTextureNames(Uint number, Vector<Uint>& textures) {
            m_textureState.GenerateNames(number, textures);
        }

        const SharedPtr<ITextureObject>& GLContext::GetTextureObject(Uint index) {
            return m_textureState.GetTextureObject(index);
        }

        const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
            return m_textureState.GetDefaultTextureObject(target);
        }

        const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
            return m_textureState.CreateTextureObject(index, target);
        }

        const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
            Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
            Uint numLevels, Uint minLayer, Uint numLayers) {
            return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
                                                          numLayers);
        }

        void GLContext::MarkTextureObjectForDeletion(Uint index) {
            // GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
            // that is currently bound acts as if FramebufferTexture* had been called with texture
            // zero for every attachment point it occupied there. Framebuffers that are NOT bound
            // keep the orphaned attachment, so only the bound ones are touched.
            //
            // Without this the framebuffer object goes on holding the deleted texture alive as its
            // attachment, and a later read through that framebuffer returns the dead texture's
            // contents rather than those of whatever the application put in its place - the name
            // it deleted usually comes straight back from the next glGenTextures, so the two are
            // indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
            if (const auto& textureObject = m_textureState.GetTextureObject(index)) {
                for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
                     ++targetIndex) {
                    const auto& framebuffer =
                        GetFramebufferBindingSlot(static_cast<FramebufferTarget>(targetIndex)).GetBoundObject();
                    if (!framebuffer || framebuffer->IsDefaultFramebuffer()) {
                        continue;
                    }
                    const auto& attachments = framebuffer->GetAllAttachmentObjects();
                    for (SizeT i = 0; i < attachments.size(); ++i) {
                        if (attachments[i].IsTexture() && attachments[i].GetTexture() == textureObject) {
                            framebuffer->Detach(static_cast<FramebufferAttachmentType>(i));
                        }
                    }
                }
            }
            m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
        }

        TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
            return m_textureState.GetUnitObject(unit);
        }

        ImageTextureBinding& GLContext::GetImageTextureBinding(Int unit) {
            return m_textureState.GetImageTextureBinding(unit);
        }

        const ImageTextureBinding& GLContext::GetImageTextureBinding(Int unit) const {
            return m_textureState.GetImageTextureBinding(unit);
        }

        Bool GLContext::ValidateTextureName(Uint index) const {
            return m_textureState.ValidateName(index);
        }

        Bool GLContext::ValidateTextureObject(Uint index) const {
            return m_textureState.ValidateTextureObject(index);
        }

        Int GLContext::GetActiveTextureUnit() const {
            return m_textureState.GetActiveTextureUnit();
        }

        void GLContext::SetActiveTextureUnit(Int unit) {
            m_textureState.SetActiveTextureUnit(unit);
        }

        // Program
        Uint GLContext::CreateProgram() {
            return m_programState.CreateProgram();
        }

        Uint GLContext::CreateShader(const ShaderStage stage) {
            return m_programState.CreateShader(stage);
        }

        void GLContext::MarkProgramForDeletion(const Uint index) {
            return m_programState.MarkProgramObjectForDeletion(index);
        }

        void GLContext::MarkShaderForDeletion(const Uint index) {
            return m_programState.MarkShaderObjectForDeletion(index);
        }

        void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
            return m_programState.ReleaseShaderNameIfOrphaned(index);
        }

        Bool GLContext::ValidateProgramName(const Uint index) const {
            return m_programState.ValidateProgramObject(index);
        }

        Bool GLContext::ValidateShaderName(const Uint index) const {
            return m_programState.ValidateShaderObject(index);
        }

        const SharedPtr<ProgramObject>& GLContext::GetProgramObject(const Uint index) {
            return m_programState.GetProgramObject(index);
        }

        const SharedPtr<ShaderObject>& GLContext::GetShaderObject(const Uint index) {
            return m_programState.GetShaderObject(index);
        }

        void GLContext::JoinAllPendingShaderWork() {
            m_programState.JoinAllPendingWork();
        }

        void GLContext::UseProgram(Uint program) {
            return m_programState.UseProgram(program);
        }

        const SharedPtr<ProgramObject>& GLContext::GetCurrentProgram() {
            return m_programState.GetCurrentProgram();
        }

        // Copies every default-block uniform value `source` holds into the same-named uniform of
        // `destination`, by name and by location.
        //
        // The composite a pipeline draws through is a DIFFERENT program object from the stage
        // programs the application writes uniforms to - glUniform* addresses the pipeline's
        // active program and glProgramUniform* addresses a named one, neither of which is the
        // composite - so without this a pipeline draw reads the composite's zero defaults and
        // paints them. Values are COPIED rather than aliased: the two programs' global UBOs are
        // laid out independently (the composite merges several stages' uniforms into one block,
        // so the same uniform sits at a different offset in each), and a copy also means the
        // composite can outlive a stage program without ever pointing into freed storage.
        //
        // Location-by-location so that arrays are carried across whole, and via the padded
        // storage span so a mat3's std140 column padding travels with it.
        //
        // WHICH uniforms: exactly the ones `source` has been WRITTEN to since its last link
        // (ProgramObject's per-location dirty set), and that restriction is a correctness fix
        // as much as it is the reason this is cheap.
        //
        // SSO gives each stage program its own storage for a uniform, so two stage programs
        // may declare the same name and hold different values - but the composite is one link
        // with one slot for it, and RefreshCompositeUniforms walks the stages in order. When
        // every active uniform was copied unconditionally, the LAST graphics stage that merely
        // DECLARED a name won, even while holding nothing but GL's zero default, and an
        // earlier stage's written value was overwritten with zeros on the way to the draw. The
        // shared-header idiom - the same `uniform mat4 u_mvp` declared in the VS and the FS,
        // written through glActiveShaderProgram(pipe, vs) - rendered nothing because of it.
        // Copying only written uniforms makes that case, which is the overwhelmingly common
        // one, simply correct: an unwritten declaration has nothing to say and says nothing.
        //
        // WHEN BOTH STAGES WROTE THE SAME NAME there is no single right answer available -
        // GL_ARB_separate_shader_objects gives the two values separate storage and the
        // composite has one slot - so the rule is LAST WRITTEN-TO GRAPHICS STAGE WINS, in
        // ShaderStage enum order (Vertex .. Fragment), decided by the stage walk in
        // RefreshCompositeUniforms. It is deterministic, and it is strictly better than what
        // it replaces: only a stage that actually holds an application-written value can now
        // take the slot. True last-WRITE-wins would need a global write ordering the dirty set
        // does not carry.
        //
        // An unwritten uniform is not left to chance either: the composite links the same
        // shader objects the stages do, so its own link seeds it with the same declared
        // initializers (ApplyUniformInitialValues), which is precisely the value GL says an
        // unwritten uniform reads.
        static void MirrorUniformValues(ProgramObject& source, ProgramObject& destination) {
            if (!source.GetLinkStatus() || !destination.GetLinkStatus()) return;

            // Settle both sides' phase B BEFORE taking a reference into `source`'s artifacts
            // below: these four getters are the join gate, and a join runs the phase-B publish.
            // Nothing that publish does marks a uniform today, but the loop holds a reference to
            // a Vector that a mark would push_back to, and "the replay does not mark" is not a
            // property a future reader of this line can see.
            const char* sourceUbo = static_cast<const char*>(source.GetUBOData());
            char* destinationUbo = static_cast<char*>(destination.MapUBO());
            const SizeT sourceUboSize = source.GetUBOSize();
            const SizeT destinationUboSize = destination.GetUBOSize();

            // O(uniforms written), not O(uniforms declared). The two name lookups below are
            // string hashes into both programs' location maps, and doing them for every active
            // uniform of every stage on every gate trip was hundreds of them per draw on a
            // large program. A stage nothing has been written to costs one empty() test.
            //
            // FALLBACK, and it is load-bearing rather than defensive: a program only records
            // its writes once something asks it to be separable (ProgramObject::SetSeparable
            // arms the latch), but glUseProgramStages here validates only LINK_STATUS - it does
            // not reject a program that was never linked as separable, which GL 4.6 core 7.4
            // says it should. So a plain glCreateProgram/glLinkProgram program CAN be installed
            // as a stage, and it will have recorded nothing at all. Mirroring "only what was
            // written" would then mirror nothing and paint the composite's defaults - a fresh
            // regression on a shape that worked. For such a program the old full walk is exactly
            // right: it has no dirty set to be more precise with.
            const Bool byWriteSet = source.TracksUniformWrites();
            const Vector<Uint>& writtenIndices = source.GetWrittenUniformIndices();
            const Uint uniformCount = source.GetUniformCount();
            const SizeT indexCount = byWriteSet ? writtenIndices.size() : static_cast<SizeT>(uniformCount);
            if (indexCount == 0) return;

            for (SizeT slot = 0; slot < indexCount; ++slot) {
                const Uint index = byWriteSet ? writtenIndices[slot] : static_cast<Uint>(slot);
                const String& name = source.GetActiveUniformName(index);
                if (name.empty()) continue;
                const Int sourceBase = source.GetUniformLocation(name);
                const Int destinationBase = destination.GetUniformLocation(name);
                // A uniform the composite's own link dropped (or renamed) is simply not
                // mirrored; the draw cannot read what does not exist.
                if (sourceBase < 0 || destinationBase < 0) continue;

                const GLint arraySize = source.GetActiveUniformArraySize(index);
                const Int elements = arraySize > 0 ? static_cast<Int>(arraySize) : 1;
                for (Int element = 0; element < elements; ++element) {
                    const Int sourceLocation = sourceBase + element;
                    const Int destinationLocation = destinationBase + element;
                    if (!source.IsValidUniformLocation(sourceLocation) ||
                        !destination.IsValidUniformLocation(destinationLocation)) {
                        break;
                    }
                    // Per ELEMENT, not per array: `arr[3] = x` must carry element 3 and leave
                    // the elements another stage owns alone. `continue`, not `break` - the
                    // written elements of an array need not be a prefix of it.
                    if (byWriteSet && !source.IsUniformWrittenAtLocation(static_cast<Uint>(sourceLocation))) {
                        continue;
                    }
                    // Stop at the end of EITHER side's array rather than walking onto the
                    // neighbouring uniform of whichever program has the shorter one.
                    if (!source.UniformLocationsAliasSameUniform(sourceBase, sourceLocation) ||
                        !destination.UniformLocationsAliasSameUniform(destinationBase, destinationLocation)) {
                        break;
                    }

                    const Bool sourceOpaque = source.IsUniformOpaqueAtLocation(sourceLocation);
                    if (sourceOpaque != destination.IsUniformOpaqueAtLocation(destinationLocation)) break;
                    if (sourceOpaque) {
                        // A sampler/image unit is phase-A state, not UBO bytes. The setter
                        // itself is a no-op when the value already matches, so this does not
                        // churn the composite's backend state version.
                        destination.SetUniformSamplerOrImageUnitIndex(
                            destinationLocation, source.GetUniformSamplerOrImageUnitIndex(sourceLocation));
                        continue;
                    }

                    const SizeT span = source.GetUniformStorageSpanInBytes(sourceLocation);
                    if (span == 0 || span != destination.GetUniformStorageSpanInBytes(destinationLocation)) continue;
                    const Uint sourceOffset = source.GetUniformOffset(sourceLocation);
                    const Uint destinationOffset = destination.GetUniformOffset(destinationLocation);
                    // Either side can legitimately lack backing storage: the optimizer deletes a
                    // uniform nothing reads, and a program whose SPIR-V phase settled cancelled
                    // has no shadow at all. Both report kInvalidUniformOffset / a null shadow.
                    if (sourceUbo == nullptr || destinationUbo == nullptr ||
                        sourceOffset == ProgramObject::kInvalidUniformOffset ||
                        destinationOffset == ProgramObject::kInvalidUniformOffset ||
                        sourceOffset + span > sourceUboSize || destinationOffset + span > destinationUboSize) {
                        continue;
                    }
                    if (std::memcmp(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span) == 0) {
                        continue;
                    }
                    Memcpy(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span);
                    destination.MarkUBOContentDirty();
                }
            }
        }

        // The other half of "the composite is a different program object": interface BLOCK
        // bindings. glUniformBlockBinding and glShaderStorageBlockBinding place a block on a
        // binding point, and they do it per program - so a pipeline whose blocks were placed
        // that way drew against the composite's own bindings, which come from the shader
        // declarations alone. A block declared without any layout(binding) therefore sat on
        // whatever the declaration implied while the application's buffers sat somewhere else,
        // and nothing anywhere raised an error: the draw simply read or wrote the wrong place.
        //
        // Both sides seed these from the same shader declarations at link, so mirroring a block
        // the application never rebound writes back the value the destination already holds and
        // the setters' equality checks make it free.
        static void MirrorBlockBindings(const ProgramObject& source, ProgramObject& destination) {
            // Storage blocks are keyed by GL name on both sides - the one coordinate the
            // frontend, SPIR-V and driver index spaces all agree on - so this is a direct
            // replay. Empty for the overwhelming majority of programs.
            for (const auto& [blockName, binding] : source.GetShaderStorageBlockBindingOverrides()) {
                if (binding < 0) continue;
                destination.SetShaderStorageBlockBinding(blockName, static_cast<Uint>(binding));
            }

            // Uniform blocks are keyed by index, and the two programs number them
            // independently, so they are matched by name.
            const Int sourceBlockCount = source.GetActiveUniformBlocksCount();
            for (Int sourceIndex = 0; sourceIndex < sourceBlockCount; ++sourceIndex) {
                const Int binding = static_cast<Int>(source.GetUniformBlockBinding(static_cast<Uint>(sourceIndex)));
                // -1 is "no declared binding and never rebound" - there is nothing to carry,
                // and forwarding it would land as binding 0xFFFFFFFF.
                if (binding < 0) continue;
                const String& blockName = source.GetUniformBlockName(static_cast<Uint>(sourceIndex));
                if (blockName.empty()) continue;
                const Uint destinationIndex = destination.GetUniformBlockIndex(blockName.c_str());
                if (destinationIndex == 0xFFFFFFFFu) continue; // GL_INVALID_INDEX
                destination.SetUniformBlockBinding(destinationIndex, static_cast<Uint>(binding));
            }
        }

        // Brings the pipeline's composite up to date with the per-program state its stage
        // programs hold and it does not: uniform values, and interface block bindings. Runs on
        // every draw through a pipeline, so the common case is the version compare below and
        // nothing else.
        static void RefreshCompositeUniforms(ProgramPipelineObject& pipeline, const SharedPtr<ProgramObject>& composite) {
            if (!composite) return;
            const auto versions = pipeline.ComputeUniformMirrorVersions();
            if (versions == pipeline.GetMirroredUniformVersions()) return;

            // A program bound to two stages appears twice; mirroring it twice would be
            // idempotent but is still work, and the second pass would have nothing to do.
            Array<ProgramObject*, ProgramPipelineObject::kGraphicsStageCount> mirrored{};
            SizeT mirroredCount = 0;
            for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
                const auto& stageProgram = pipeline.GetStageProgram(static_cast<ShaderStage>(stage));
                if (!stageProgram) continue;
                Bool alreadyMirrored = false;
                for (SizeT i = 0; i < mirroredCount; ++i) {
                    if (mirrored[i] == stageProgram.get()) {
                        alreadyMirrored = true;
                        break;
                    }
                }
                if (alreadyMirrored) continue;
                mirrored[mirroredCount++] = stageProgram.get();
                MirrorUniformValues(*stageProgram, *composite);
                MirrorBlockBindings(*stageProgram, *composite);
            }
            pipeline.SetMirroredUniformVersions(versions);
        }

        const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
            static const SharedPtr<ProgramObject> nullProgram = nullptr;
            const auto& currentProgram = m_programState.GetCurrentProgram();
            if (currentProgram) {
                // P1 join site J1, plain glUseProgram half. The backends read a program's
                // lifetimeId / backendStateVersion / UBO content version to decide whether
                // their per-program caches are still valid, and none of those pass through
                // ProgramObject's join gate - so a draw could sample a version, join later
                // inside the same draw when it finally touched an artifact, and cache under a
                // version the publish had already superseded. Settling here means every
                // version a backend reads during a draw describes the program it is drawing.
                // Two null checks in steady state.
                //
                // BOTH phases, and that is not optional: the phase-B publish bumps those same
                // versions, so joining only phase A here would leave exactly the hazard this
                // site exists to close - a backend samples a version, then trips the phase-B
                // gate through GetGeneratedSpirv() deeper inside the same draw, and memoizes
                // under a version the publish has already superseded.
                currentProgram->JoinLinkAndSpirv();
                return currentProgram;
            }
            if (m_boundProgramPipeline == 0) return nullProgram;
            const auto& pipeline = GetBoundProgramPipeline();
            if (!pipeline) return nullProgram;

            // P1 join site J1. ComputeDrawProgramSignature() keys the composite cache on each
            // stage program's lifetimeId and linkVersion - NON-artifact fields, so they do not
            // pass through ProgramObject's join gate and a pending link would stay pending
            // right through the signature. Since the version is bumped both at enqueue and at
            // publish, the signature computed inside a pending window is one that will never
            // be produced again: every draw would miss the cache and rebuild (and relink) the
            // composite. Join first, so the signature describes settled programs. In steady
            // state this is a null check per stage.
            for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
                const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
                if (stageProgram) stageProgram->JoinLinkAndSpirv();
            }

            const auto signature = pipeline->ComputeDrawProgramSignature();
            if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) {
                RefreshCompositeUniforms(*pipeline, cached);
                return cached;
            }

            // Everything downstream of here - the backends, the uniform plumbing, the draw
            // validation - is written against a single linked program, so the pipeline is
            // flattened into one. Each stage contributes only the shaders that serve it, so a
            // program bound to two stages is not pulled in twice and a program bound to a
            // stage it does not implement contributes nothing.
            // Deliberately not a named program: it is reachable only through the pipeline, it
            // must not answer glIsProgram, and it must not consume a name the application
            // could otherwise be handed. Backend registries key on the object, not the name.
            auto composite = MakeShared<ProgramObject>(0u);

            // GRAPHICS stages only. A pipeline may carry a compute stage alongside them (GL
            // 4.6 core 7.4 forbids linking compute WITH another stage into one program, not
            // attaching a compute program to a pipeline that also has graphics ones), and that
            // stage belongs to glDispatchCompute, not to this draw. Compositing it in produced
            // a graphics program carrying a compute module, which Adreno 830 does not reject
            // from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
            Bool anyStage = false;
            // Which stages the composite ACTUALLY got a shader for. Not the same question as
            // "which stages have a stage program bound": one program bound with
            // GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
            // stages it was linked with. The transform-feedback capture stage is chosen off this,
            // because it has to be the stage that will exist in the composite's own link.
            Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
            for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
                const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
                if (!stageProgram) continue;
                // The stage program contributes the shaders its LAST LINK consumed, never
                // its live attach list: per GL 4.6 7.3/7.4 a pipeline stage executes the
                // stage program as last linked - glAttachShader and glCompileShader take
                // effect only at the program's next link - and neither of those moves the
                // link version this cache keys on, so reading live state here would let a
                // post-link attach or recompile leak into the composite while the signature
                // still hits. The pinned (source, node) makes the composite's Link()
                // consume the very inputs that link consumed.
                for (const auto& ref : stageProgram->GetLinkedShaderSnapshot()) {
                    if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
                    composite->AttachShaderWithPinnedLinkInput(ref);
                    anyStage = true;
                    compositeHasStage[stage] = true;
                }
            }
            if (!anyStage) return nullProgram;
            // Transform feedback captures the output of the LAST vertex-processing stage
            // (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
            // only the stage program carrying that stage can have been given. The composite is
            // assembled out of the stage programs' shaders and inherits none of their
            // GL-thread-owned state, so without this it links with an empty capture list and
            // glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
            // no transform feedback varyings") even though glValidateProgramPipeline had passed.
            //
            // TWO RULES, both easy to get subtly wrong and both load-bearing:
            //
            // (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
            //     take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
            //     deliberately bumps no version - so a request written after the stage program's
            //     last link is invisible to the composite cache's signature yet would be picked up
            //     by the next rebuild, making the capture list depend on whether some unrelated
            //     event happened to invalidate the cache. Worse, a name that is not an output of
            //     the capture stage fails the composite's OWN link, and a failed composite makes
            //     every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
            //     snapshot removes the whole class: linked state only moves at a link, and a link
            //     is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
            //     the existing cache key is sufficient by construction.
            //     GetTransformFeedbackInterfaceNames() is the right accessor rather than the
            //     resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
            //     (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
            //
            // (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
            //     the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
            //     getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
            //     WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
            //     Skipping a geometry stage that has no capture list and installing the vertex
            //     stage's instead made them disagree, and the composite then resolved a vertex
            //     program's names against the geometry intermediate - capturing where GL says it
            //     must not, or failing the link and killing every draw. A capture stage with an
            //     empty list is not a reason to look further down: it is the answer, and
            //     glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
            //
            // The order is the pipeline read backwards and includes the tessellation CONTROL
            // stage, which is a vertex-processing stage too (GL 4.6 core 11): it can only be
            // the last one in a pipeline that has a TCS but no evaluation or geometry stage,
            // which is why it sits after TessEval. Same four stages, same order, as
            // ProgramLinkTask::ResolveTransformFeedbackVaryings - see rule (2).
            for (const ShaderStage captureStage:
                 {ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
                  ShaderStage::Vertex}) {
                if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
                const auto& captureProgram = pipeline->GetStageProgram(captureStage);
                if (!captureProgram) continue;
                const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
                if (!linkedNames.empty()) {
                    composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
                                                            captureProgram->GetTransformFeedbackBufferMode());
                }
                break;
            }
            // A pipeline with no fragment stage still rasterises, so the default fragment
            // shader is wanted here even though the separable stage programs never get one.
            composite->Link(true);
            // P1 join site J2. The draw that asked for this program is the very next thing to
            // happen, so enqueueing the composite's link buys nothing and only moves the wait
            // to whichever backend accessor happens to touch its artifacts first. Both phases,
            // for the same reason: the backend is about to read its SPIR-V.
            composite->JoinLinkAndSpirv();
            pipeline->SetCachedDrawProgram(signature, Move(composite));
            const auto& cached = pipeline->GetCachedDrawProgram(signature);
            RefreshCompositeUniforms(*pipeline, cached);
            return cached;
        }

        const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() {
            static const SharedPtr<ProgramObject> nullProgram = nullptr;
            const auto& currentProgram = m_programState.GetCurrentProgram();
            if (currentProgram) {
                // Same join contract as GetProgramForDraw's glUseProgram half - see the note
                // there. A dispatch reads the same non-artifact versions a draw does.
                currentProgram->JoinLinkAndSpirv();
                return currentProgram;
            }
            if (m_boundProgramPipeline == 0) return nullProgram;
            const auto& pipeline = GetBoundProgramPipeline();
            if (!pipeline) return nullProgram;
            // No compositing and no cache: GL 4.6 core 7.4 makes a compute program exclusive of
            // every other stage, so the pipeline's compute stage program IS the program to
            // dispatch, uniforms and all. That also means glUniform* through the active program
            // lands on the very object the dispatch reads - the composite's uniform refresh has
            // no counterpart to do here.
            const auto& computeProgram = pipeline->GetStageProgram(ShaderStage::Compute);
            if (!computeProgram) return nullProgram;
            computeProgram->JoinLinkAndSpirv();
            return computeProgram;
        }

        const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
            const auto& currentProgram = m_programState.GetCurrentProgram();
            if (currentProgram) return currentProgram;
            static const SharedPtr<ProgramObject> nullProgram = nullptr;
            if (m_boundProgramPipeline == 0) return nullProgram;
            const auto& pipeline = GetBoundProgramPipeline();
            if (!pipeline) return nullProgram;
            return pipeline->GetActiveProgram();
        }

        // RenderState
        Uint GLContext::GetPipelineStateVersion() const {
            return m_renderState.GetPipelineStateVersion();
        }

        Uint GLContext::GetRenderStateParametersVersion() const {
            return m_renderState.GetVersion();
        }

        const RenderStateParameters& GLContext::GetRenderStateParameters() const {
            return m_renderState.GetAllParameters();
        }

        void GLContext::SetViewport(IntVec4 viewport) {
            m_renderState.SetViewport(viewport);
        }

        IntVec4 GLContext::GetViewport() const {
            return m_renderState.GetViewport();
        }

        void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) {
            m_renderState.SetViewportIndexed(index, viewport);
        }

        const FloatVec4& GLContext::GetViewportIndexed(Uint index) const {
            return m_renderState.GetViewportIndexed(index);
        }

        void GLContext::SetLineWidth(Float width) {
            m_renderState.SetLineWidth(width);
        }

        Float GLContext::GetLineWidth() const {
            return m_renderState.GetLineWidth();
        }

        void GLContext::SetHint(GLenum target, GLenum mode) {
            m_renderState.SetHint(target, mode);
        }

        GLenum GLContext::GetHint(GLenum target) const {
            return m_renderState.GetHint(target);
        }

        void GLContext::SetPointFadeThresholdSize(Float size) {
            m_renderState.SetPointFadeThresholdSize(size);
        }

        Float GLContext::GetPointFadeThresholdSize() const {
            return m_renderState.GetPointFadeThresholdSize();
        }

        void GLContext::SetPointSpriteCoordOrigin(GLenum origin) {
            m_renderState.SetPointSpriteCoordOrigin(origin);
        }

        GLenum GLContext::GetPointSpriteCoordOrigin() const {
            return m_renderState.GetPointSpriteCoordOrigin();
        }

        void GLContext::SetClampReadColor(GLenum clamp) {
            m_renderState.SetClampReadColor(clamp);
        }

        GLenum GLContext::GetClampReadColor() const {
            return m_renderState.GetClampReadColor();
        }

        void GLContext::SetPolygonMode(GLenum front, GLenum back) {
            m_renderState.SetPolygonMode(front, back);
        }

        GLenum GLContext::GetPolygonModeFront() const {
            return m_renderState.GetPolygonModeFront();
        }

        GLenum GLContext::GetPolygonModeBack() const {
            return m_renderState.GetPolygonModeBack();
        }

        void GLContext::SetPrimitiveRestartIndex(Uint32 index) {
            m_renderState.SetPrimitiveRestartIndex(index);
        }

        Uint32 GLContext::GetPrimitiveRestartIndex() const {
            return m_renderState.GetPrimitiveRestartIndex();
        }

        void GLContext::SetPointSize(Float size) {
            m_renderState.SetPointSize(size);
        }

        void GLContext::SetPatchVertices(Uint vertices) {
            m_renderState.SetPatchVertices(vertices);
        }

        void GLContext::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
            m_renderState.SetPatchDefaultOuterLevel(levels);
        }

        const FloatVec4& GLContext::GetPatchDefaultOuterLevel() const {
            return m_renderState.GetPatchDefaultOuterLevel();
        }

        void GLContext::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
            m_renderState.SetPatchDefaultInnerLevel(levels);
        }

        const FloatVec2& GLContext::GetPatchDefaultInnerLevel() const {
            return m_renderState.GetPatchDefaultInnerLevel();
        }

        Uint GLContext::GetPatchVertices() const {
            return m_renderState.GetPatchVertices();
        }

        Float GLContext::GetPointSize() const {
            return m_renderState.GetPointSize();
        }

        void GLContext::SetPolygonOffset(Float factor, Float units) {
            m_renderState.SetPolygonOffset(factor, units);
        }

        Float GLContext::GetPolygonOffsetFactor() const {
            return m_renderState.GetPolygonOffsetFactor();
        }

        Float GLContext::GetPolygonOffsetUnits() const {
            return m_renderState.GetPolygonOffsetUnits();
        }

        void GLContext::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
            m_renderState.SetPolygonOffsetClamped(factor, units, clamp);
        }

        Float GLContext::GetPolygonOffsetClamp() const {
            return m_renderState.GetPolygonOffsetClamp();
        }

        void GLContext::SetClipControl(GLenum origin, GLenum depth) {
            m_renderState.SetClipControl(origin, depth);
        }

        GLenum GLContext::GetClipOrigin() const {
            return m_renderState.GetClipOrigin();
        }

        GLenum GLContext::GetClipDepthMode() const {
            return m_renderState.GetClipDepthMode();
        }

        void GLContext::SetCapability(CapabilityInput cap, Bool enabled) {
            m_renderState.SetCapability(cap, enabled);
        }

        Bool GLContext::IsCapabilityEnabled(CapabilityInput cap) const {
            return m_renderState.IsCapabilityEnabled(cap);
        }

        void GLContext::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
            m_renderState.SetCapabilityIndexed(cap, index, enabled);
        }

        Bool GLContext::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
            return m_renderState.IsCapabilityEnabledIndexed(cap, index);
        }

        void GLContext::SetBlendFunc(BlendFactor srcRGB, BlendFactor dstRGB, BlendFactor srcAlpha,
                                     BlendFactor dstAlpha) {
            m_renderState.SetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
        }

        void GLContext::GetBlendFunc(BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
                                     BlendFactor& dstAlpha) const {
            m_renderState.GetBlendFunc(srcRGB, dstRGB, srcAlpha, dstAlpha);
        }

        void GLContext::SetBlendFuncIndexed(Uint index, BlendFactor srcRGB, BlendFactor dstRGB, BlendFactor srcAlpha,
                                            BlendFactor dstAlpha) {
            m_renderState.SetBlendFuncIndexed(index, srcRGB, dstRGB, srcAlpha, dstAlpha);
        }

        void GLContext::GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
                                            BlendFactor& dstAlpha) const {
            m_renderState.GetBlendFuncIndexed(index, srcRGB, dstRGB, srcAlpha, dstAlpha);
        }

        void GLContext::SetBlendEquation(BlendEquation color, BlendEquation alpha) {
            m_renderState.SetBlendEquation(color, alpha);
        }

        void GLContext::GetBlendEquation(BlendEquation& color, BlendEquation& alpha) const {
            m_renderState.GetBlendEquation(color, alpha);
        }

        void GLContext::SetBlendEquationIndexed(Uint index, BlendEquation color, BlendEquation alpha) {
            m_renderState.SetBlendEquationIndexed(index, color, alpha);
        }

        void GLContext::GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
            m_renderState.GetBlendEquationIndexed(index, color, alpha);
        }

        void GLContext::SetLogicOp(LogicOperation logicOp) {
            m_renderState.SetLogicOp(logicOp);
        }

        LogicOperation GLContext::GetLogicOp() const {
            return m_renderState.GetLogicOp();
        }

        void GLContext::SetDepthFunc(DepthTestFunc func) {
            m_renderState.SetDepthFunc(func);
        }

        DepthTestFunc GLContext::GetDepthFunc() const {
            return m_renderState.GetDepthFunc();
        }

        void GLContext::SetDepthMask(Bool flag) {
            m_renderState.SetDepthMask(flag);
        }

        Bool GLContext::GetDepthMask() const {
            return m_renderState.GetDepthMask();
        }

        void GLContext::SetStencilFunc(StencilFace face, DepthTestFunc func, Int ref, Uint32 mask) {
            m_renderState.SetStencilFunc(face, func, ref, mask);
        }

        void GLContext::SetStencilMask(StencilFace face, Uint32 mask) {
            m_renderState.SetStencilMask(face, mask);
        }

        void GLContext::SetStencilOp(StencilFace face, StencilOperation fail, StencilOperation depthFail,
                                     StencilOperation depthPass) {
            m_renderState.SetStencilOp(face, fail, depthFail, depthPass);
        }

        const StencilFaceState& GLContext::GetStencilState(StencilFace face) const {
            return m_renderState.GetStencilState(face);
        }

        void GLContext::SetColorMask(BoolVec4 mask) {
            m_renderState.SetColorMask(mask);
        }

        BoolVec4 GLContext::GetColorMask() const {
            return m_renderState.GetColorMask();
        }

        void GLContext::SetColorMaskIndexed(Uint index, BoolVec4 mask) {
            m_renderState.SetColorMaskIndexed(index, mask);
        }

        BoolVec4 GLContext::GetColorMaskIndexed(Uint index) const {
            return m_renderState.GetColorMaskIndexed(index);
        }

        void GLContext::SetClearColor(FloatVec4 color) {
            m_renderState.SetClearColor(color);
        }

        const FloatVec4& GLContext::GetClearColor() const {
            return m_renderState.GetClearColor();
        }

        void GLContext::SetClearDepth(Float depth) {
            m_renderState.SetClearDepth(depth);
        }

        Float GLContext::GetClearDepth() const {
            return m_renderState.GetClearDepth();
        }

        void GLContext::SetClearStencil(Int stencil) {
            m_renderState.SetClearStencil(stencil);
        }

        Uint32 GLContext::GetClearStencil() const {
            return m_renderState.GetClearStencil();
        }

        void GLContext::SetBlendColor(FloatVec4 color) {
            m_renderState.SetBlendColor(color);
        }

        const FloatVec4& GLContext::GetBlendColor() const {
            return m_renderState.GetBlendColor();
        }

        void GLContext::SetDepthRange(FloatVec2 range) {
            m_renderState.SetDepthRange(range);
        }

        const FloatVec2& GLContext::GetDepthRange() const {
            return m_renderState.GetDepthRange();
        }

        void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
            m_renderState.SetDepthRangeIndexed(index, range);
        }

        const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const {
            return m_renderState.GetDepthRangeIndexed(index);
        }

        void GLContext::SetSampleCoverage(Float value, Bool invert) {
            m_renderState.SetSampleCoverage(value, invert);
        }

        Float GLContext::GetSampleCoverageValue() const {
            return m_renderState.GetSampleCoverageValue();
        }

        Bool GLContext::GetSampleCoverageInvert() const {
            return m_renderState.GetSampleCoverageInvert();
        }

        void GLContext::SetSampleMaskValue(Uint32 mask) {
            m_renderState.SetSampleMaskValue(mask);
        }

        Uint32 GLContext::GetSampleMaskValue() const {
            return m_renderState.GetSampleMaskValue();
        }

        void GLContext::SetMinSampleShadingValue(Float value) {
            m_renderState.SetMinSampleShadingValue(value);
        }

        Float GLContext::GetMinSampleShadingValue() const {
            return m_renderState.GetMinSampleShadingValue();
        }

        void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) {
            m_renderState.SetPixelStoreParam(param, value);
        }

        Int GLContext::GetPixelStoreParam(PixelStoreParam param) const {
            return m_renderState.GetPixelStoreParam(param);
        }

        PixelStoreParameters GLContext::GetPixelStoreParameters(Bool isUnpack) const {
            return m_renderState.GetPixelStoreParameters(isUnpack);
        }

        void GLContext::SetCullFaceMode(CullFaceMode mode) {
            m_renderState.SetCullFaceMode(mode);
        }

        CullFaceMode GLContext::GetCullFaceMode() const {
            return m_renderState.GetCullFaceMode();
        }

        void GLContext::SetFrontFaceMode(FrontFaceMode mode) {
            m_renderState.SetFrontFaceMode(mode);
        }

        FrontFaceMode GLContext::GetFrontFaceMode() const {
            return m_renderState.GetFrontFaceMode();
        }

        void GLContext::SetProvokingVertexMode(ProvokingVertexMode mode) {
            m_renderState.SetProvokingVertexMode(mode);
        }

        ProvokingVertexMode GLContext::GetProvokingVertexMode() const {
            return m_renderState.GetProvokingVertexMode();
        }

        void GLContext::SetScissorBox(IntVec4 box) {
            m_renderState.SetScissorBox(box);
        }

        const IntVec4& GLContext::GetScissorBox() const {
            return m_renderState.GetScissorBox();
        }

        void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) {
            m_renderState.SetScissorBoxIndexed(index, box);
        }

        const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const {
            return m_renderState.GetScissorBoxIndexed(index);
        }

        // Framebuffer
        void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
            m_framebufferState.GenerateNames(number, framebuffers);
        }

        const SharedPtr<FramebufferObject>& GLContext::GetFramebufferObject(Uint index) {
            return m_framebufferState.GetFramebufferObject(index);
        }

        BindingSlot<FramebufferObject>& GLContext::GetFramebufferBindingSlot(FramebufferTarget target) {
            return m_framebufferState.GetBindingSlot(target);
        }

        const SharedPtr<FramebufferObject>& GLContext::CreateFramebufferObject(Uint index) {
            return m_framebufferState.CreateFramebufferObject(index);
        }

        void GLContext::MarkFramebufferObjectForDeletion(Uint index) {
            m_framebufferState.MarkFramebufferObjectForDeletion(index);
        }

        Bool GLContext::ValidateFramebufferName(Uint index) const {
            return m_framebufferState.ValidateName(index);
        }

        Bool GLContext::ValidateFramebufferObject(Uint index) const {
            return m_framebufferState.ValidateFramebufferObject(index);
        }

        // Sampler
        void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) {
            m_samplerState.GenerateNames(number, samplers);
        }

        const SharedPtr<SamplerObject>& GLContext::GetSamplerObject(Uint index) {
            return m_samplerState.GetSamplerObject(index);
        }

        const SharedPtr<SamplerObject>& GLContext::CreateSamplerObject(Uint index) {
            return m_samplerState.CreateSamplerObject(index);
        }

        void GLContext::MarkSamplerObjectForDeletion(Uint index) {
            // Unbind the sampler from all texture units
            if (ValidateSamplerObject(index)) {
                auto sampler = m_samplerState.GetSamplerObject(index);
                for (Int unit = 0; unit < TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) {
                    auto& textureUnit = m_textureState.GetUnitObject(unit);
                    if (textureUnit.GetSamplerObject() == sampler) {
                        textureUnit.SetSamplerObject(nullptr);
                    }
                }
            }
            m_samplerState.MarkSamplerObjectForDeletion(index);
        }

        Bool GLContext::ValidateSamplerName(Uint index) const {
            return m_samplerState.ValidateName(index);
        }

        Bool GLContext::ValidateSamplerObject(Uint index) const {
            return m_samplerState.ValidateSamplerObject(index);
        }

        // Renderbuffer
        void GLContext::GenRenderbufferNames(Uint number, Vector<Uint>& renderbuffers) {
            m_renderbufferState.GenerateNames(number, renderbuffers);
        }

        const SharedPtr<RenderbufferObject>& GLContext::GetRenderbufferObject(Uint index) {
            return m_renderbufferState.GetRenderbufferObject(index);
        }

        BindingSlot<RenderbufferObject>& GLContext::GetRenderbufferBindingSlot(RenderbufferTarget target) {
            return m_renderbufferState.GetBindingSlot(target);
        }

        const SharedPtr<RenderbufferObject>& GLContext::CreateRenderbufferObject(Uint index) {
            return m_renderbufferState.CreateRenderbufferObject(index);
        }

        void GLContext::MarkRenderbufferObjectForDeletion(Uint index) {
            m_renderbufferState.MarkRenderbufferObjectForDeletion(index);
        }

        Bool GLContext::ValidateRenderbufferName(Uint index) const {
            return m_renderbufferState.ValidateName(index);
        }

        Bool GLContext::ValidateRenderbufferObject(Uint index) const {
            return m_renderbufferState.ValidateRenderbufferObject(index);
        }

        void GLContext::SaveBoundTransformFeedbackState() {
            auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
            for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
                const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
                object.bindings[i] = {point.GetBoundObject(), point.GetRange(), point.HasExplicitRange()};
            }
            object.active = m_transformFeedbackActive;
            object.paused = m_transformFeedbackPaused;
            object.primitiveMode = m_transformFeedbackPrimitiveMode;
            object.program = m_transformFeedbackProgram;
            object.generation = m_transformFeedbackGeneration;
            object.capturedVertices = m_transformFeedbackCapturedVertices;
            object.inputPrimitives = m_transformFeedbackInputPrimitives;
        }

        void GLContext::RestoreBoundTransformFeedbackState() {
            const auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
            for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
                auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
                point.Bind(object.bindings[i].buffer);
                if (object.bindings[i].buffer) {
                    point.SetRange(object.bindings[i].range, object.bindings[i].hasExplicitRange);
                } else {
                    point.ClearRange();
                }
            }
            m_transformFeedbackActive = object.active;
            m_transformFeedbackPaused = object.paused;
            m_transformFeedbackPrimitiveMode = object.primitiveMode;
            m_transformFeedbackProgram = object.program;
            // The generation identifies one capture span, and a span belongs to the object
            // that opened it - a backend keys its append state on it, so switching objects
            // has to bring the right one back.
            m_transformFeedbackGeneration = object.generation;
            m_transformFeedbackCapturedVertices = object.capturedVertices;
            m_transformFeedbackInputPrimitives = object.inputPrimitives;
        }

        void GLContext::GenTransformFeedbackNames(Uint number, Vector<Uint>& ids) {
            ids.resize(number);
            if (number == 0) return;
            m_transformFeedbackNames.Generate(number, ids.data());
            // A generated name already denotes an object with the default state, so that a
            // bind never has to distinguish "first use" from any later one.
            for (const Uint id : ids) {
                m_transformFeedbackObjects[id] = {};
            }
        }
        // Program pipeline
        void GLContext::GenProgramPipelineNames(Uint number, Vector<Uint>& pipelines) {
            pipelines.resize(number);
            // Names only. The OBJECT appears as soon as a command needs somewhere to put state
            // (see MaterializeProgramPipelineObject), but glIsProgramPipeline still answers
            // GL_FALSE until the name is bound or created - see IsProgramPipelineObject.
            m_programPipelineNames.Generate(number, pipelines.data());
        }

        void GLContext::CreateProgramPipelineObject(Uint index) {
            const auto object = MakeShared<ProgramPipelineObject>(index);
            // glCreateProgramPipelines makes the object outright, so it answers
            // glIsProgramPipeline immediately - unlike a name that only got here through
            // GenProgramPipelines plus a command that materialized it.
            object->MarkEverBound();
            m_programPipelines[index] = object;
        }

        Bool GLContext::ValidateProgramPipelineName(Uint index) const {
            return index == 0 || m_programPipelineNames.IsValid(index);
        }

        // glIsProgramPipeline. Materialization is NOT the test: the object now appears as soon
        // as any command takes state from a reserved name, and two of those commands are the
        // pure queries glGetProgramPipelineiv / glGetProgramPipelineInfoLog - so keying this on
        // map membership would let merely READING a gen'd name turn it into an object. GL 4.6
        // core 7.4 gives the real rule: a GenProgramPipelines name acquires program pipeline
        // state when it is first bound. Same shape as IsTransformFeedbackObject.
        Bool GLContext::IsProgramPipelineObject(Uint index) const {
            if (index == 0 || !m_programPipelineNames.IsValid(index)) return false;
            const auto it = m_programPipelines.find(index);
            return it != m_programPipelines.end() && it->second && it->second->GetEverBound();
        }

        void GLContext::BindProgramPipelineObject(Uint index) {
            if (index != 0) {
                if (const auto& object = MaterializeProgramPipelineObject(index)) {
                    object->MarkEverBound();
                }
            }
            m_boundProgramPipeline = index;
        }

        // Binding is not the only thing that turns a reserved name into an object. GL 4.6 core
        // 7.4 asks of UseProgramStages, ActiveShaderProgram and ValidateProgramPipeline only that
        // the name came from GenProgramPipelines and has not been deleted - so a name that was
        // reserved and never bound must take state from them, not be rejected. glIsProgramPipeline
        // is the one place the distinction survives (it answers FALSE until the name is used),
        // which is why IsProgramPipelineObject stays as it is.
        const SharedPtr<ProgramPipelineObject>& GLContext::MaterializeProgramPipelineObject(Uint index) {
            static const SharedPtr<ProgramPipelineObject> kNone;
            if (index == 0 || !m_programPipelineNames.IsValid(index)) return kNone;
            const auto it = m_programPipelines.find(index);
            if (it != m_programPipelines.end()) return it->second;
            return m_programPipelines[index] = MakeShared<ProgramPipelineObject>(index);
        }

        void GLContext::MarkProgramPipelineForDeletion(Uint index) {
            if (index == 0 || !m_programPipelineNames.IsValid(index)) return;
            if (index == m_boundProgramPipeline) {
                m_boundProgramPipeline = 0;
            }
            m_programPipelines.erase(index);
            m_programPipelineNames.Delete(index);
        }

        const SharedPtr<ProgramPipelineObject>& GLContext::GetProgramPipelineObject(Uint index) const {
            static const SharedPtr<ProgramPipelineObject> kNone;
            const auto it = m_programPipelines.find(index);
            return it == m_programPipelines.end() ? kNone : it->second;
        }

        const SharedPtr<ProgramPipelineObject>& GLContext::GetBoundProgramPipeline() const {
            return GetProgramPipelineObject(m_boundProgramPipeline);
        }


        Bool GLContext::ValidateTransformFeedbackName(Uint index) const {
            return index == 0 || m_transformFeedbackNames.IsValid(index);
        }

        void GLContext::BindTransformFeedbackObject(Uint index) {
            if (index == m_boundTransformFeedback) return;
            SaveBoundTransformFeedbackState();
            m_boundTransformFeedback = index;
            m_transformFeedbackObjects[index].everBound = true;
            RestoreBoundTransformFeedbackState();
        }

        Bool GLContext::IsTransformFeedbackObject(Uint index) const {
            if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return false;
            const auto it = m_transformFeedbackObjects.find(index);
            return it != m_transformFeedbackObjects.end() && it->second.everBound;
        }

        void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) {
            if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return;
            // Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1);
            // its state is dropped rather than saved back into the dying object.
            if (index == m_boundTransformFeedback) {
                m_boundTransformFeedback = 0;
                RestoreBoundTransformFeedbackState();
            }
            m_transformFeedbackObjects.erase(index);
            m_transformFeedbackNames.Delete(index);
        }

        Uint64 GLContext::GetTransformFeedbackRecordedVertices(Uint index) const {
            const auto it = m_transformFeedbackObjects.find(index);
            return it == m_transformFeedbackObjects.end() ? 0 : it->second.recordedVertices;
        }

        Bool GLContext::HasTransformFeedbackCompletedSpan(Uint index) const {
            const auto it = m_transformFeedbackObjects.find(index);
            return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan;
        }

        void GLContext::CreateTransformFeedbackObject(Uint index) {
            // glCreateTransformFeedbacks has no bind step to infer existence from, so the name it
            // hands out is already the name of an object (GL 4.6 core 13.2.1).
            m_transformFeedbackObjects[index] = {};
            m_transformFeedbackObjects[index].everBound = true;
        }

        Bool GLContext::IsNamedTransformFeedbackActive(Uint index) const {
            if (index == m_boundTransformFeedback) return m_transformFeedbackActive;
            const auto it = m_transformFeedbackObjects.find(index);
            return it != m_transformFeedbackObjects.end() && it->second.active;
        }

        Bool GLContext::IsNamedTransformFeedbackPaused(Uint index) const {
            if (index == m_boundTransformFeedback) return m_transformFeedbackPaused;
            const auto it = m_transformFeedbackObjects.find(index);
            return it != m_transformFeedbackObjects.end() && it->second.paused;
        }

        NamedTransformFeedbackBinding GLContext::GetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex) const {
            NamedTransformFeedbackBinding result;
            if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return result;
            // The bound object's capture bindings live in the context's own binding points, not in
            // the saved copy - that one is only written when the object is swapped out.
            if (index == m_boundTransformFeedback) {
                const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
                result.Buffer = point.GetBoundObject();
                result.Range = point.GetRange();
                result.HasExplicitRange = point.HasExplicitRange();
                return result;
            }
            const auto it = m_transformFeedbackObjects.find(index);
            if (it == m_transformFeedbackObjects.end()) return result;
            const auto& saved = it->second.bindings[bufferIndex];
            result.Buffer = saved.buffer;
            result.Range = saved.range;
            result.HasExplicitRange = saved.hasExplicitRange;
            return result;
        }

        void GLContext::SetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex,
                                                         const SharedPtr<BufferObject>& buffer, Range1D range,
                                                         Bool hasExplicitRange) {
            if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return;
            if (index == m_boundTransformFeedback) {
                auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
                point.Bind(buffer);
                if (buffer && hasExplicitRange) {
                    point.SetRange(range, true);
                } else {
                    point.ClearRange();
                }
                return;
            }
            auto& object = m_transformFeedbackObjects[index];
            object.bindings[bufferIndex] = {buffer, range, hasExplicitRange};
        }
    } // namespace GLState

    // Leak-at-exit storage; see GlobalObjects.cpp.
    UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
} // namespace MobileGL::MG_State
