// MobileGL - MobileGL/MG_Backend/DirectGLES/Utils.h
// 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

#pragma once
#include <Includes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>

namespace MobileGL::MG_Backend::DirectGLES {
    namespace DebugImpl {
        class ErrorLopper {
        public:
            static void Loop(const std::function<void(GLenum)>&);
            static void Clear();
            ErrorLopper();
            ~ErrorLopper();
        };

        class OpenGLScopeMarker {
        public:
            explicit OpenGLScopeMarker(const String& scopeName);
            ~OpenGLScopeMarker();
        };
    } // namespace DebugImpl

    namespace BufferImpl {} // namespace BufferImpl

    namespace VertexArrayImpl {
        GLenum GetBindingQuery(GLenum target, bool isTexture);
    } // namespace VertexArrayImpl

    namespace TextureImpl {
        // Whether images on this format-capability target can back a colour attachment, and so
        // need a colour-renderable storage format even when the frontend asked for a
        // three-channel one ES never renders to. Shared by the capability probe (which passes the
        // capabilities it has just queried, before the globals are published) and by the
        // allocation path (which reads the active backend's), so the format the cache was probed
        // with is always the format the image is created with.
        Bool TargetRequiresRenderableFormat(SizeT targetIndex);
        Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
            const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);

        void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
                                       GLenum* outFormat, GLenum* outType,
                                       TextureTarget target = TextureTarget::Unknown);
        void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
                                            GLenum* outFormat, GLenum* outType);
        Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);

        // True when the format the image is actually created with has an alpha channel the
        // frontend format does not (the three-channel colour-renderable widening). GL reads such
        // a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
        // any readback of the image has to overwrite the alpha the draw happened to leave there.
        Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
        Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
        Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);

        // The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
        // GLSL ES cannot spell as an image is carried by one it can.
        //
        // GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
        // GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
        // ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
        // (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
        // Seventeen have a core format of the SAME per-channel width and component type,
        // differing only in channel count, and in one of those the emulation is EXACT: GL already
        // defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
        // dropping the components the format does not have, so the carrier's surplus channels
        // hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
        // is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
        // half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
        //
        // Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
        // for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
        // r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
        // diagnostic rather than a silent approximation), and on a driver that HAS
        // GL_NV_image_formats, where the shader keeps the declared format and no widening may
        // happen behind it.
        //
        // The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
        // renderability substitution: an image that cannot be image-bound is useless whatever its
        // attachment behaviour, so the image constraint wins. In practice that only bites
        // RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
        // signed-normalized instead of becoming the half float that fallback would have picked -
        // so an image-bound texture in one of those two formats is no longer attachable, and
        // glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
        // does not update. Accepted deliberately: before the widening, an image binding in either
        // format was refused outright by every driver tested and the stage that declared it never
        // compiled at all, so nothing that works today is being given up.
        //
        // KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
        // is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
        // it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
        // (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
        // glReadPixels/glGetTexImage that asks for more channels than the frontend format has
        // would see them. Closing it needs the per-draw-buffer colour mask the three-channel
        // widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
        // from "alpha" to a channel count, which is its own change.
        // How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
        // transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
        // shadow already holds SourceChannels components of exactly the carrier's own type, so
        // padding it out to four is the whole conversion. The packed entries do not - their shadow
        // is ONE 32-bit word per texel - and reading such a word as components of the carrier's
        // type takes twelve or sixteen bytes out of four and shears the level.
        enum class ImageWidenSourceEncoding : Uint8 {
            Components = 0,
            // r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
            PackedFloat11f11f10f,
            // rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
            // channel CODES of an rgba16ui. The same split serves both: the two formats differ
            // only in what the codes MEAN, which is the shader's business and not the transfer's.
            PackedInt2101010Rev,
        };

        struct ImageBindableStorageWidening {
            GLenum InternalFormat = GL_UNKNOWN_MGL;
            GLenum Format = GL_UNKNOWN_MGL;
            GLenum Type = GL_UNKNOWN_MGL;
            // Channels the FRONTEND format has, i.e. how many of the carrier's four the client
            // data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
            Uint SourceChannels = 0;
            // Whether that implied 1 is the integer one or a saturated normalized field - the
            // transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
            // RG8UI), so the carrier decides.
            Bool IntegerData = false;
            // What the upload has to do to the frontend shadow before it describes the level to
            // the driver (PrepareImageWidenedUpload).
            ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
            // Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
            // NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
            // has no image format of any width for and which a float carrier would requantise.
            // Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
            // 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
            //
            // Two things depend on it, both because the ES storage no longer shares the frontend
            // format's component class: the upload pads a missing alpha with ChannelMax[3] instead
            // of the transfer type's own "one" (through a uint carrier the saturated field IS the
            // one), and glGetTexImage divides the codes back out into the floats the application
            // is still owed.
            Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
            Bool SignedNormalized = false;

            Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
            explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
        };
        ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);

        // The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
        // GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
        //
        // A buffer texture cannot be widened: its texels are the application's buffer object, at
        // the size and layout the application gave it, and it is usually also a vertex, index or
        // storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
        // an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
        // how the shader subscripts them - component j of texel i is texel 2i + j of the base
        // view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
        // widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
        // nothing.
        //
        // KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
        // buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
        // sampled view split too, and the sampler side is not rewritten. Accepted for the same
        // reason the storage widening's gaps are - on a driver where the split applies at all
        // there is no legal ESSL for the image declaration, so such a program did not compile.
        GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
    } // namespace TextureImpl

    namespace FramebufferImpl {} // namespace FramebufferImpl

    // Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
    // RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
    // exercise the exact packing the GL CTS packed_pixels oracle compares against.
    namespace ReadbackImpl {
        struct ReadbackChannelMapping {
            Int sourceChannel[4]; // RGBA source channel feeding each destination component
            Int channelCount;     // destination component count
            Bool isInteger;
        };
        Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);

        // Byte size of one destination component of `type`; packed types report the packed word size.
        // 0 = type not supported by the conversion path.
        SizeT GetReadbackComponentSize(GLenum type);

        // Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
        // component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
        // the packed word: non-REV types pack the first component from the MSB, *_REV types from the
        // LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
        struct PackedReadbackLayout {
            Int fieldCount;     // format components stored in the packed word
            Int width[4];       // bit width of each component's field
            Int shift[4];       // LSB bit position of each component's field
            SizeT byteSize;     // packed word size in bytes (1, 2 or 4)
            Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
        };
        Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);

        // Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
        Uint32 EncodeFloatToUnsignedF11(Float value);
        Uint32 EncodeFloatToUnsignedF10(Float value);
        Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);

        // Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
        // not convertible (unknown type, packed field count != format component count, floating-point
        // or packed-float type with an integer format).
        SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);

        // Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
        // client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
        // bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
        void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
                                    const ReadbackChannelMapping& mapping, GLenum type);

        // Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
        // honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
        // swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
        // DirectGLES and DirectVulkan readback conversion paths.
        Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
                                   GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
                                   void* pixels, Bool applyPackImageParams);

        // Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
        // parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
        // storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
        // through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
        // the shared exponent, so glGetTexImage would answer with different bits than were stored.
        // `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
        // False when `type` is not a 4-byte packed type.
        Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
                                      GLenum type, void* pixels, Bool applyPackImageParams);
    } // namespace ReadbackImpl

    namespace PrgramImpl {
        String ProcessOutColorLocations(const String& glslCode);
        String ForceSupporterOutput(const String& glslCode);
        String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
                                        Uint32 unormOutputMask);
        String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
        // Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
        // 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
        // which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
        // outputs and copies the value into them at the end of main. A no-op for
        // drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
        // enables several draw buffers, so the ordinary single-target shader is untouched.
        String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
        // SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
        // sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
        // On a driver that advertises only GL_OES_texture_buffer that directive is a compile
        // error, so the name is retargeted in the emitted source. A no-op on every other tier:
        // ES 3.2 needs no directive at all and an EXT driver already has the right one.
        String RetargetTextureBufferExtension(String glslCode,
                                              MG_External::GLESCapabilities::TextureBufferTier tier);
        // Adds `#extension GL_NV_image_formats : require` when the shader carries an image
        // format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
        // asks for nothing, so the request has to be made here. `needed` is the caller's answer,
        // because only it knows which formats are in play AND whether the driver advertises the
        // extension - requesting an unadvertised extension is itself a compile error, so this is
        // never emitted speculatively. A no-op when not needed or already present.
        String RequestExtendedImageFormats(String glslCode, Bool needed);
        // Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
        // gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
        // gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
        // spelling for it at any version, so the request has to be made here or the stage does
        // not compile - which loses the whole program, not just the multi-viewport routing.
        // `needed` is the caller's answer for the same reason as above: only it knows whether the
        // driver advertises the extension, and requesting an unadvertised one is itself a compile
        // error, so this is never emitted speculatively. A no-op when not needed or already
        // present.
        String RequestViewportArrayExtension(String glslCode, Bool needed);
        // Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
        // emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
        // vertex-processing stage; ESSL does NOT have it in those two at any version - not even
        // 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
        // EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
        // asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
        // fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
        // program 0 - the draw renders nothing and any transform-feedback capture it was carrying
        // is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
        // advertises neither spelling, because requesting an unadvertised extension is itself a
        // compile error. A no-op when nullptr or already present.
        String RequestPointSizeExtension(String glslCode, const char* extensionName);
        // The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
        // PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
        // ones; the two extensions are separate and neither implies the other.
        const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
        // Writes a format layout qualifier into the image declarations named in
        // `esslFormatByUniformName` that still have none. The completion half of the image-format
        // bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
        // format in, but SPIRV-Cross throws rather than printing the formats it calls
        // desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
        // KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
        // stage. So those formats stay out of the module and are spelled here instead, on the
        // emitted text, where nothing can refuse them.
        //
        // Declarations that already carry a format are left exactly as they are, whoever wrote
        // it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
        // stops being safe to edit by hand.
        String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
        String RemoveLayoutBinding(const String& glslCode);
        // Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
        // image array into; the suffix is the array's own name and the element's index.
        constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
        // One image ARRAY whose elements the application pointed at units that are not
        // consecutive-from-element-zero.
        struct ImageArrayUnitPlan {
            String name;       // the array's name, exactly as the emitted ESSL declares it
            Vector<Int> units; // the frontend image unit element k has to reach
        };
        // Desktop GL lets an application give each element of an image array an ARBITRARY unit
        // (glUniform1i per element). ES has no such call at all - "ES image units come
        // exclusively from the layout(binding=N) qualifier" - and one declaration carries one
        // binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
        // MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
        // they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
        // 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
        // wrong value and three were never written, with no GL error and no link log. The same
        // defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
        // array has no API side to fix, because ES makes glUniform1i on an image uniform an
        // INVALID_OPERATION.
        //
        // Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
        // its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
        // element k. One declaration carries one binding, so one declaration per unit is the
        // only spelling that reaches an arbitrary set of them.
        //
        // That rewrite needs every k in the emitted text to be a LITERAL, and it is:
        // LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
        // image-array subscript in the module, because ESSL forbids one outright ("image arrays
        // indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
        // ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
        // widening the array to cover the whole span of units and routing each subscript through
        // a `const highp int` offset table - was written before that pass covered images, and
        // the table lookup was itself one of the non-constant expressions the same probe refuses.
        // The split also costs exactly the image uniforms the application declared, where the
        // widening cost the whole SPAN (seven for the four elements of
        // KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
        // fail to fit in.
        //
        // Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
        // caller to report - when the emitted extent disagrees with the reflection, when the
        // array is reached by anything other than a subscript, or when a subscript is not a
        // literal element index. Silence was the whole defect here, so a decline must be audible.
        //
        // Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
        // key on the GL uniform name and on a binding already being stamped) and BEFORE
        // SplitReadWriteImageUniforms (so each element that is both read and written is split
        // with its own binding already on it) and RemoveLayoutBinding (which is what preserves
        // image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
        // per-program units it reads need no entry in BuildEsslTranslationKey.
        String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
                                           Vector<String>* outDeclined = nullptr);
        // The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
        // the text between the braces, verbatim - or nullopt when the shader does not redeclare
        // the block in that direction. `input` selects the `in gl_PerVertex` form over the
        // `out` one.
        //
        // Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
        // rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
        // which members, depends on what the application's shader touched; a synthesized stage
        // that redeclares a different shape than its neighbours is an ES link error against a
        // program that has no other problem.
        std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
        // The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
        // patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
        // the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
        //
        // Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
        // PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
        // rejects a program that has an evaluation stage without a control stage - with an EMPTY
        // info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
        // frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
        // bound in its place, and every draw silently renders nothing.
        //
        // `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
        // and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
        // ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
        // alone, which is what matching a neighbour that did not redeclare requires.
        //
        // All four outer levels and both inner levels are written unconditionally: writing a
        // level the evaluation stage's domain does not use is legal and ignored, and it saves
        // this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
        // GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
        // glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
        // That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
        // is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
        //
        // The same stage, for the same reason, that DirectVulkan synthesizes in
        // ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
        // tessellation stages. Kept as two generators rather than one because the two targets
        // disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
        // gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
        // VkShaderModule against a driver shader object.
        String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
                                               const String& inPerVertexMembers,
                                               const String& outPerVertexMembers,
                                               const FloatVec4& defaultOuterLevel,
                                               const FloatVec2& defaultInnerLevel);
        // Prefix of the writeonly half a read+write image uniform is split into (see
        // SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
        constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
        // The three names SplitReadWriteImageUniforms renames a rewritten image declaration
        // under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
        // own accesses, so two stages that use an image the same way arrive at the SAME name and
        // two that use it differently arrive at different ones - which is exactly the property
        // the rename exists for, at no cost to the stages that agree. Exposed for the tests.
        constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
        constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
        constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
        // ESSL refuses an image variable that carries a format qualifier other than r32f /
        // r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
        // 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
        // SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
        // speculatively decorates every storage image NonWritable+NonReadable
        // (fixup_image_load_store_access), then OpImageRead clears NonReadable and
        // OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
        // from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
        // bare declaration, so the frontend raises no error and the illegal ESSL only shows
        // up as a device compile failure - and then as a silently no-op draw.
        //
        // Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
        //  * loaded only            -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
        //  * stored only            -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
        //  * both                   -> emit TWO declarations on the same binding and of the
        //                              same type, `coherent readonly
        //                              <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
        //                              writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
        //                              every imageStore at the second one, and follow each of
        //                              those stores with `memoryBarrierImage();`. Several image
        //                              variables may share an image unit as long as they have
        //                              the same type and format, which is exactly what the pair
        //                              is.
        //
        // The rename is the other half of the repair and applies to all three cases. The qualifier
        // chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
        // declared in two stages to be declared identically - so a shader that stores an image from
        // the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
        // and `readonly` in the other. Adreno merges the two same-named declarations and silently
        // drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
        // still reads back its initial contents
        // (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
        // isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
        // carry `coherent`). Renaming leaves no cross-stage variable to merge.
        //
        // The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
        // point: two stages that use an image the same way emit byte-identical declarations, so
        // letting them keep one shared name costs nothing and merging them is correct, while two
        // stages that use it differently land on different prefixes and cannot be merged at all.
        // A per-STAGE tag also satisfied the first requirement but violated the second: it made
        // the SAME image a distinct uniform in every stage that named it, and Adreno allocates
        // image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
        // binding_images_texture_type_* declares three read+write images in each of its five
        // stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
        // linker answered "Error: Image Image location or component exceeds max allowed. Error:
        // Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
        // glslang's link, surfaced only as every draw silently doing nothing and the images
        // reading back zero. Mali and Mesa link the same text, so nothing but a device gate
        // catches this.
        //
        // A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
        // repair, but it is NOT stage-independent: that tag is derived from the accesses of the
        // stage being emitted, so an image stored in the vertex stage and loaded in the fragment
        // stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
        // one name, two spellings, which is exactly the pair Adreno merges. Those declarations
        // are therefore renamed too, keyed on the qualifier they already carry (readonly ->
        // IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
        // nothing but the identifier changed. Stages that agree still reach the same alias and
        // stay merged, so this costs no shader an extra image uniform.
        //
        // The declarations this pass still leaves untouched keep their names: one carrying BOTH
        // readonly and writeonly (a spelling no access analysis produces, so it came from the
        // application and is identical everywhere), and one carrying NEITHER, which is legal only
        // for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
        //
        // The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
        // guarantees a write through one image variable is visible to a read through a DIFFERENT
        // one when both are coherent, and the split is what makes a same-variable
        // read-after-write cross-variable. The single-declaration repairs above do not get it -
        // nothing aliases them.
        //
        // The barrier is the other half of the same problem, and coherent alone did not cover it:
        // visibility is not ORDER. Within one invocation the ES compiler sees a write to one
        // variable and a read of another it has no reason to believe alias, and is free to serve
        // the read from before the write - which is what advanced-memory-order's store/load/
        // compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
        // GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
        // non-uniform control flow. It costs something in a shader that stores to a read+write
        // image in a loop, which is why it is confined to the split pair.
        //
        // Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
        // a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
        // shader that used to compile into a link failure. ES only guarantees 4 fragment image
        // uniforms, so a shader with more than half the limit in read+write images is the case
        // to watch.
        //
        // Runs on the transpiled ESSL, so it must see the bindings the frontend units were
        // already rewritten to and must run before those bindings are stripped - see the call
        // site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
        // stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
        //
        // `outSplitCount`, when given, receives the number of declarations that were actually
        // doubled - i.e. exactly how many image uniforms this stage gained over what the
        // application declared. Zero for every shader but a handful, and the only number the
        // budget note above can be reported with.
        String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
        // Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
        // the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
        constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
        // ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
        // only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
        // shader as a uniform and be folded into every lookup's level of detail. Declares
        // one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
        // it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
        // the bound texture's (or sampler object's) value into it; a shader whose samplers
        // all have a zero bias is therefore unaffected. Returns the source unchanged when
        // there is nothing to rewrite.
        //
        // avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
        // so their constant level stays constant; only the implicit-LOD forms take the bias.
        // Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
        // into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
        String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
    } // namespace PrgramImpl

    namespace Utils {
        void CheckGLESError();
        GLenum GetBindingQuery(GLenum target, bool isTexture);
    } // namespace Utils
} // namespace MobileGL::MG_Backend::DirectGLES
