// MobileGL - MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.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 "SpvcSession.h"
#include "glslang/TVarEntryInfo.h"
#include "glslang/TMglGlslIoResolver.h"

#include <map>
#include <set>

namespace MobileGL {
    namespace MG_Util {
        namespace ShaderTranspiler {
            // A GLSL float literal for a tessellation level, for the pass-through tessellation
            // control stage both backends synthesize when a program has an evaluation stage and no
            // control stage. Shared so the two generators cannot disagree about what a level means.
            //
            // GL 4.6 core 11.2.2 discards a patch only when a relevant OUTER level is <= 0; every
            // other value is CLAMPED into [1, MAX_TESS_GEN_LEVEL]. So "draw nothing" is reserved
            // for the values that really mean it, and everything else has to survive the trip
            // through text: a shortest-round-trip spelling, because a fixed-decimal one flushes
            // small positive levels to zero, and always with a '.' or an exponent, because a bare
            // digit sequence is an INT literal and `gl_TessLevelOuter[0] = 1;` does not compile.
            String TessellationLevelLiteral(Float value);

            class ShaderCompiler {
            public:
                static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
                static Result<SharedPtr<glslang::TProgram>> LinkProgram(const ProgramAttrib& attrib);
                static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
                // `nativeFloat64` is the caller's FINAL verdict, not a capability read: true means
                // the two fp64 passes at the tail of the chain are skipped and real doubles reach
                // the driver. False - which is DirectGLES always, every mobile device, and the
                // no-backend default - runs the chain exactly as it always has. It is the ONE
                // argument of this function that changes the output bytes, which is why it is
                // also a field of the L1 memo's key.
                //
                // Production sets it in ProgramSpirvTask::GenerateSpirv, which takes the verdict
                // for the WHOLE program (CompileEnv::ConsumesFloat64Natively() minus the
                // 64-bit-vertex-input exception) before touching any module. Do not re-derive it
                // per module: the global UBO is one buffer every stage reads.
                static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
                                                      Vector<uint32_t>& outputBinary,
                                                      bool validateOutput = true,
                                                      bool enableSpirvValidation = false,
                                                      bool nativeFloat64 = false);
                // Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
                // (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
                // Only for backends without native draw-parameter support (DirectGLES).
                static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
                                                       Vector<uint32_t>& outputBinary,
                                                       bool enableSpirvValidation = false);
                // Demotes the gl_ViewportIndex OUTPUT builtin to a plain Private global named
                // mg_ViewportIndex, so SPIRV-Cross emits an ordinary declaration instead of a bare
                // gl_ViewportIndex that ESSL has no core spelling for. Multi-viewport routing is
                // lost (everything lands in viewport 0) but the stage compiles and the program
                // runs, instead of every draw made with it becoming a silent no-op. Only for the
                // DirectGLES transpile path on a driver WITHOUT GL_OES_viewport_array; gl_Layer is
                // deliberately left alone, being core in ESSL 3.20 geometry shaders.
                static bool LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
                                                      Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
                // whether the pass above has anything to do. The gate that keeps every other
                // stage off an optimizer round trip it does not need.
                static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
                // Clamps the Sample image-operand of every multisample fetch to the sample count
                // the BACKEND can really deliver for that image's category, which on Adreno and
                // Mali is 1 for integer formats while the frontend advertises the GL-mandated
                // floor of 4. Without it a `texelFetch(usampler2DMS, coord, 3)` reads past the
                // end of a one-sample allocation. Pass the backend-real per-category ceilings and
                // the advertised maximum (GL_Getter's GetAdvertisedMaxSamples); a category that
                // already reaches the advertised value is left alone. DirectGLES transpile path
                // only. See ClampMultisampleFetchPass.
                static bool ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
                                                           Vector<uint32_t>& outputBinary,
                                                           Int32 maxColorSamples,
                                                           Int32 maxIntegerSamples,
                                                           Int32 maxDepthSamples,
                                                           Int32 advertisedMaxSamples,
                                                           bool enableSpirvValidation = false);
                // Whether the module declares any multisampled image type, i.e. whether the pass
                // above has anything to do. The gate that keeps every other stage off an
                // optimizer round trip it does not need.
                static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
                // Both gate questions above answered from ONE parse. Every armed gate costs a
                // BuildModule per shader stage, and on a driver where both are armed (Mali: no
                // GL_OES_viewport_array AND integer multisample squeezed to 1) the separate
                // probes made compile-heavy workloads measurably slower - ReservedNames-class
                // CTS cases paid ~10%. Callers with more than one armed gate use this instead.
                // The image-format widening deliberately does NOT ride this probe, even though it
                // is a module question of exactly the same shape. It is armed on every driver, so
                // a gate answered from the module would put a BuildModule on every stage of every
                // program - and the frontend's uniform reflection can answer it for free
                // (PrgramImpl::ImageFormatBakeInputs::declaresWidenableImageFormat).
                struct SpirvGateFeatures {
                    Bool WritesViewportIndexOutput = false;
                    Bool DeclaresMultisampledImage = false;
                };
                static SpirvGateFeatures ProbeSpirvGateFeatures(const Vector<Uint32>& binary);
                // Replaces an ARRAY vertex input with one input per element at consecutive
                // locations, seeding a Private copy of the array so indexed reads still work.
                // GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
                // rather than emulating them, so without this the stage never reaches the
                // driver. Only for the DirectGLES transpile path.
                static bool SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
                                                          Vector<uint32_t>& outputBinary,
                                                          bool enableSpirvValidation = false);
                // Replaces the named interface BLOCKS with one variable per member, named
                // "<Block>_<member>", shadowing the block itself so the body is untouched. The
                // Adreno ES driver silently captures NOTHING for a transform-feedback varying
                // named as a block member, so a capture list that names one has to be respelled
                // - and the declaration with it. `flattenedBlockNames` reports which blocks
                // this stage actually rewrote, which is what the capture list must follow.
                // Only for the DirectGLES transpile path.
                static bool FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
                                                             const std::set<String>& blockNames,
                                                             std::set<String>& flattenedBlockNames,
                                                             Vector<uint32_t>& outputBinary,
                                                             bool enableSpirvValidation = false);
                // The capture request "StageData.attrib[0]" as the pass above renamed it,
                // "StageData_attrib[0]", or false when it does not name a member of a block
                // that was flattened.
                static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName,
                                                                   const std::set<String>& flattenedBlockNames,
                                                                   String& outName);
                // Adds to `collidingBlockNames` every inter-stage interface block this stage
                // declares in BOTH directions at once (`in FOO {...}; out FOO {...}`, which
                // desktop GLSL allows because its input and output block namespaces are
                // separate), and to `declaredNames` every name the module spells. The gate for
                // UniquifyIoBlockNamesForEssl below, and the source of the name set a
                // replacement has to avoid. Reads the module; never rewrites it.
                static void ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
                                                     std::set<String>& collidingBlockNames,
                                                     std::set<String>& declaredNames);
                // Renames inter-stage interface BLOCK types so the collision the probe above
                // found gets one spelling per producing stage. `inputBlockRenames` applies to
                // blocks this stage consumes and `outputBlockRenames` to blocks it produces,
                // both planned program-wide by the caller so a producer and its consumer keep
                // matching; `renamedBlockNames` reports the original names this stage actually
                // rewrote. SPIRV-Cross re-emits two same-named blocks verbatim and the Mali ES
                // driver then loses the output block's payload. Only for the DirectGLES
                // transpile path. See UniquifyIoBlockNamesPass.
                static bool UniquifyIoBlockNamesForEssl(const Vector<Uint32>& inputBinary,
                                                        const std::map<String, String>& inputBlockRenames,
                                                        const std::map<String, String>& outputBlockRenames,
                                                        std::set<String>& renamedBlockNames,
                                                        Vector<uint32_t>& outputBinary,
                                                        bool enableSpirvValidation = false);
                // Drops the Location (and Component) decoration from inter-stage interface
                // BLOCK variables, so SPIRV-Cross emits them unqualified and ES matches them
                // by block name plus member sequence. `stripInputBlocks` covers the blocks
                // this stage consumes and `stripOutputBlocks` the ones it produces - armed
                // separately because an interface whose other end is in a DIFFERENT program
                // must keep the location that matches it there. `strippedAny` reports whether
                // this stage actually had one. The Mali ES driver loses the payload of a
                // located block across any tessellation or geometry boundary; only for the
                // DirectGLES transpile path, and only when the driver POST says so. See
                // StripIoBlockLocationsPass.
                static bool StripIoBlockLocationsForEssl(const Vector<Uint32>& inputBinary,
                                                         bool stripInputBlocks, bool stripOutputBlocks,
                                                         bool& strippedAny, Vector<uint32_t>& outputBinary,
                                                         bool enableSpirvValidation = false);
                // Drops RelaxedPrecision member decorations from uniform-block structs so
                // SPIRV-Cross prints the same (highp) member precision in every stage; ES
                // drivers reject cross-stage uniform blocks whose member precisions differ.
                // Only for the DirectGLES transpile path.
                static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
                                                                  Vector<uint32_t>& outputBinary,
                                                             bool enableSpirvValidation = false);
                // Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
                // DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
                // (SPIRV-Cross would otherwise require that extension and the driver would reject it).
                static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
                                                      Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // Emulates noperspective (screen-linear) interpolation via gl_Position.w / gl_FragCoord.w
                // so no NV extension is needed; strips what it cannot emulate. DirectGLES fallback for
                // devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
                static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
                                                        Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // Makes every index into a fragment-output array a constant integral
                // expression, which is what GLSL ES requires and SPIR-V does not. Runs the
                // stock folding chain first (loop unrolling folds the loop-derived indices
                // real shaders use), and lowers whatever is left - a genuinely dynamic index -
                // to a switch over the array's range. DirectGLES transpile path only: the
                // original module is legal for Vulkan, and no other stage is constrained this
                // way. Copies the input through untouched when no fragment output is indexed
                // dynamically, which is every shader but a handful.
                // See LegalizeFragmentOutputIndexPass.
                static bool LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
                                                                  Vector<uint32_t>& outputBinary,
                                                             bool enableSpirvValidation = false);
                // Makes every index into an ARRAY OF SHADER STORAGE BLOCKS or an ARRAY OF IMAGE
                // UNIFORMS a constant integral expression. Desktop GL allows any
                // dynamically-uniform index in either; ES keeps the ES 3.1
                // constant-expression rule for both and the drivers refuse the whole stage
                // ("indexing into an SSBO array using a non-constant expression is not
                // permitted" on Qualcomm, "image arrays indexed with non-constant expressions
                // are forbidden in GLSL ES" on Mesa), which loses the program while the
                // frontend still reports GL_LINK_STATUS = TRUE. Same two halves as the
                // fragment-output legalization: fold the loop-derived indices, then lower
                // whatever is genuinely dynamic to a switch over the array's range. SAMPLER
                // arrays are out of scope - ESSL 3.20 4.1.7 permits them a dynamically-uniform
                // index. DirectGLES transpile path only - Vulkan has no such restriction and
                // must keep seeing one descriptor array. Copies the input through untouched
                // when no such array is indexed dynamically, which is every shader but a
                // handful. See LegalizeResourceArrayIndexPass.
                static bool LegalizeResourceArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
                                                                     Vector<uint32_t>& outputBinary,
                                                                     bool enableSpirvValidation = false);
                // Collapses each synthesized gl_AtomicCounterBlock_<N> into one uint array at
                // offset 0, re-indexing every counter access to the element that used to sit at
                // its byte offset. glslang preserves the application's layout(offset = N) as the
                // member's Offset decoration, no std140/std430 layout can express a first member
                // at a non-zero offset, and GLSL ES has no member layout(offset=) - so SPIRV-Cross
                // throws and takes the whole stage with it. DirectGLES transpile path only.
                // Copies the input through untouched when every counter block is already packed
                // naturally, which is every shader that omits the offset qualifier. See
                // FlattenAtomicCounterBlockPass.
                static bool FlattenAtomicCounterBlockOffsetsForEssl(const Vector<Uint32>& inputBinary,
                                                                    Vector<uint32_t>& outputBinary,
                                                                    bool enableSpirvValidation = false);
                // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
                // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
                // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
                // which wrongly includes baseInstance).
                // GL_TEXTURE_RECTANGLE emulated on a plain 2D texture, for every backend:
                // divides the coordinate of each normalized-coordinate lookup by the texture
                // size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
                // what it declines and why.
                static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
                                            bool enableSpirvValidation = false);
                // GL_TEXTURE_1D_ARRAY storage images rewritten to the 2D-array shape the texture
                // is actually stored in on ES, with the layer moved from the coordinate's second
                // component to its third - and, when the module performs an image ATOMIC on one,
                // the non-arrayed GL_TEXTURE_1D storage image to the 2D shape with its coordinate
                // widened to (u, 0), which is the one 1D shape SPIRV-Cross does not widen itself.
                // DirectGLES transpile path only - Vulkan binds a real VK_IMAGE_VIEW_TYPE_1D(_ARRAY)
                // and must see the module unchanged. Copies the input through untouched when the
                // module declares no such image, which is every shader but a handful. See
                // Lower1DArrayImagesPass for what it declines and why.
                static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
                                                      Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // The SAMPLED-image counterpart. SPIRV-Cross widens a 1D sampler's COORDINATE for
                // ES and prints the OFFSET and GRADIENT operands with their original 1D arity, so
                // textureOffset / textureLodOffset / texelFetchOffset / textureGrad on a
                // sampler1D(Array) come out with no ESSL overload ("no matching overloaded
                // function found") and the stage is lost. Rewrites the type to 2D and widens
                // coordinate, offset and gradients together. DirectGLES transpile path only -
                // Vulkan has 1D images natively. Copies the input through untouched unless the
                // module actually carries such an operand on a 1D sampler, so a shader that only
                // samples or fetches keeps SPIRV-Cross's own correct emission. See
                // Lower1DSampledImagesPass for what it declines and why.
                static bool Lower1DSampledImagesForEssl(const Vector<Uint32>& inputBinary,
                                                        Vector<uint32_t>& outputBinary,
                                                        bool enableSpirvValidation = false);
                // Gives each format-less storage image the format bound to its image unit, so
                // the emitted ESSL can carry the format layout qualifier GLSL ES requires of
                // every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
                // maps uniform name to the glBindImageTexture format of the unit it addresses.
                // DirectGLES transpile path only - Vulkan takes an Unknown-format storage image
                // natively. See BakeImageFormatsPass for what it declines and why.
                static bool BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
                                                    const UnorderedMap<String, Uint>& glFormatByName,
                                                    Vector<uint32_t>& outputBinary,
                                                    bool enableSpirvValidation = false);
                // Whether the module declares a storage image with no format qualifier at all,
                // i.e. whether BakeImageFormatsForEssl could change anything. One module parse,
                // so the ~every shader that declares none pays no optimizer run.
                static bool DeclaresFormatlessStorageImage(const Vector<Uint32>& binary);
                // Whether the GL internal format's image-format spelling is one GLSL ES has in
                // core. False both for a format ES only reaches through GL_NV_image_formats and
                // for one with no image-format spelling at all, so a caller that has to decide
                // whether to emit the extension directive can ask this one question.
                static bool GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat);
                // The ESSL layout-qualifier spelling of a GL internal format ("r8ui", "rgba32f"),
                // empty when the format has no image-format spelling at all.
                static String EsslImageFormatSpelling(Uint glInternalFormat);
                // Whether SPIRV-Cross will print that format when it targets ESSL. It throws on
                // the ones it calls desktop-only - taking the whole stage with it - so a caller
                // must not ask BakeImageFormatsForEssl for those, and completes them in the
                // emitted text instead.
                static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
                // Re-declares every storage image whose DECLARED format GLSL ES cannot spell in
                // the core format that carries it exactly, and masks each access back to the
                // channels the original format has. The 26 formats outside the ES core set have no
                // legal ESSL spelling on any tested driver (none exposes GL_NV_image_formats), and
                // a format-less declaration is rejected too, so the stage is otherwise lost
                // whatever this backend emits. DirectGLES transpile path only - Vulkan takes the
                // declared format natively. See WidenImageFormatsPass for the table, for the nine
                // formats it deliberately does NOT widen, and for why the texture storage and the
                // glBindImageTexture argument have to move with it.
                // `onlyFormatsSpirvCrossRefusesToPrint` narrows it to the formats that have no
                // ESSL route even WITH GL_NV_image_formats, because SPIRV-Cross throws for them
                // rather than printing a token - which is the whole set a driver that advertises
                // the extension still needs. See WidenImageFormatsPass.
                static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
                                                     Vector<uint32_t>& outputBinary,
                                                     bool onlyFormatsSpirvCrossRefusesToPrint = false,
                                                     bool enableSpirvValidation = false);
                // Whether the module declares a storage image WidenImageFormatsForEssl would
                // widen, under the same mode the run would use. Costs its own module parse, so
                // the transpile path does NOT gate on this - it answers the question from the
                // frontend's uniform reflection instead, for the reason on SpirvGateFeatures.
                // Here for tests and for callers that already hold nothing but the binary.
                static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
                                                         bool onlyFormatsSpirvCrossRefusesToPrint = false);
                // The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
                // when it needs no widening or cannot be widened exactly. The single source of
                // truth for all three layers of the emulation: this one answers the shader, and
                // DirectGLES asks it again for the texture storage and the image bind, so the two
                // sides cannot drift.
                static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
                // Channels a GL image internal format really has (1-4), 0 when it is not one of
                // the forty image formats.
                static Uint ImageFormatChannelCount(Uint glInternalFormat);
                // Whether the carrier holds the format's channels as the INTEGER CODES of a
                // normalized value, and the largest code each channel can hold. See
                // WidenImageFormatsPass::NormalizedImageCarrierCodes - DirectGLES needs it for
                // both halves of the transfer, which no longer share the frontend format's
                // component class with the ES storage.
                static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
                                                        bool& outSignedNormalized);
                // The single-channel core format a non-core BUFFER image is SPLIT into, or 0. See
                // WidenImageFormatsPass::SplitCoreEsslBufferImageFormat - DirectGLES asks it for
                // glTexBuffer's internal format and for glBindImageTexture's.
                static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
                static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
                                                         Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
                // read becomes zero, which is what GL defines for a command carrying no
                // baseVertex parameter while Vulkan's builtin would report firstVertex.
                // See ZeroBaseVertexPass.
                static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
                                                    Vector<uint32_t>& outputBinary,
                                                    bool enableSpirvValidation = false);
                // Replaces compute gl_NumSubgroups loads with ceil(workgroup invocations /
                // gl_SubgroupSize). DirectVulkan only; this repairs drivers whose builtin
                // disagrees with the subgroup IDs the same dispatch emits (Adreno reports 1
                // while emitting IDs 0..7). The ceil() partition is only spec-guaranteed
                // under VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT, which
                // the caller requests whenever it is legal for the workgroup shape; see
                // DeriveNumSubgroupsPass.
                static bool DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
                                                        Vector<uint32_t>& outputBinary,
                                                        bool enableSpirvValidation = false);
                // Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
                // 32-lane virtual subgroup built from workgroup-shared memory. Last-resort
                // path for devices with NO native subgroup support, opt-in via
                // MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; a device with native subgroup
                // operations always uses them. maxWorkgroupScratchBytes bounds the shared
                // scratch the lowering may add (pass the device's
                // maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
                // minimum). See EmulateSubgroupsPass.
                static bool EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
                                                      Vector<uint32_t>& outputBinary,
                                                      Uint32 maxWorkgroupScratchBytes,
                                                      bool enableSpirvValidation = false);
                // Grows iterationRP's under-declared gl_SubgroupID-indexed scratch to the
                // subgroup count the device actually partitions into, fingerprint-gated to
                // that pack's reduction idiom; every other module - and every device whose
                // width the pack already assumed - passes through byte-identical.
                // maxWorkgroupScratchBytes bounds the growth (pass the device's
                // maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
                // minimum). See FixIterationRPSubgroupScratchPass.
                static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
                                                                   Vector<uint32_t>& outputBinary,
                                                                   Uint32 nativeSubgroupSize,
                                                                   Uint32 maxWorkgroupScratchBytes,
                                                                   bool enableSpirvValidation = false);
                // Inserts the missing workgroup rendezvous between Program 203's two
                // prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
                // unrelated and already-repaired modules pass through byte-identical.
                static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
                                                           Vector<uint32_t>& outputBinary,
                                                           bool enableSpirvValidation = false);
                // Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
                // (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
                // VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
                // buffers. Vertex stage, DirectVulkan only; pairs with the Float64 case in
                // VertexInputStateFactory::ToVkVertexFormat.
                static bool PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
                                                            Vector<uint32_t>& outputBinary,
                                                      bool enableSpirvValidation = false);
                // Adds the Invariant decoration to every Position builtin output. GL apps
                // routinely rely on cross-program position invariance for multi-pass
                // equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
                // mobile drivers that optimize per-pipeline break that without the
                // decoration. DirectVulkan only.
                static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
                                                               Vector<uint32_t>& outputBinary,
                                                             bool enableSpirvValidation = false);
                // Replaces the declared format of float storage images with Unknown and adds the
                // matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
                // shaderStorageImage*WithoutFormat features are enabled, allowing the
                // glBindImageTexture format to select the descriptor view at runtime. Integer
                // storage images deliberately keep their declared format for GL-compatible bit
                // reinterpretation paths (for example, R32F storage accessed as r32ui).
                static bool UseUnformattedFloatStorageImagesForVulkan(
                    const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
                    bool enableSpirvValidation = false);
                // Rewrites every 64-bit float in the module to a 32-bit one, preserving every
                // block offset and stride exactly (see DemoteFloat64Pass). Already part of
                // SanitizeAndOptimizeBinary, which is where production reaches it; exposed
                // separately so a test can drive the demotion on its own.
                static bool DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
                                                   Vector<uint32_t>& outputBinary,
                                                   bool enableSpirvValidation = false);
                static Result<String> DecompileShader(SpvcSession& session);

                // ---- GL_ARB_gl_spirv ----
                // Turn an APPLICATION-supplied SPIR-V module into the desktop GLSL the ordinary
                // compile pipeline consumes.
                //
                // Why a round trip rather than handing the module straight to the backends. SPIR-V
                // is not where MobileGL's pipeline STARTS: a program's whole GL-visible surface -
                // every glGetActiveUniform, every uniform location, every block index, the
                // transform-feedback layout, the default-block UBO routing - is reflected out of
                // glslang's TProgram at link (ProgramLinkTask::SnapshotGlslangReflection), and
                // glslang can only build one from a GLSL parse. Injecting the module at
                // ProgramSpirvTask instead would skip the link entirely and leave every one of
                // those queries answering nothing. Decompiling puts the application's module at
                // the head of the SAME pipeline, so reflection, the relaxed default-block
                // lowering, both backends and every memo tier work on it unchanged.
                //
                // What it costs, stated plainly: names. A module stripped of OpName (which
                // ARB_gl_spirv permits, and the conformance suite deliberately does) comes back
                // with SPIRV-Cross's generated identifiers rather than with none, so the
                // *_MAX_LENGTH queries answer those instead of 1.
                //
                // `entryPoint` selects among several OpEntryPoint of this stage's execution
                // model; an empty string means "whichever one is there". The specialization
                // constants glSpecializeShader supplied are applied in the same pass - SPIRV-Cross
                // folds each into the emitted source as a literal once Vulkan semantics are off,
                // which is exactly what "specialize, then compile" means for a GLSL consumer.
                //
                // `constantIds` and `constantValues` are the parallel arrays the entry point
                // takes. A constant id the module does not declare is GL_INVALID_VALUE per the
                // extension; it is reported through the error log rather than silently ignored.
                // Why the caller needs a REASON and not just a failure: ARB_gl_spirv splits the
                // ways specialization can fail into two groups with different GL surfaces. A bad
                // entry-point name and a constant id the module does not declare are enumerated
                // errors - GL_INVALID_VALUE, and, being errors, they must leave the shader object
                // exactly as it was. Everything else (a module SPIRV-Cross cannot translate) is a
                // COMPILE failure, reported through COMPILE_STATUS and the info log like any other
                // glCompileShader outcome. Returning one undifferentiated error is what made both
                // groups look like the second.
                enum class SpecializationFailure {
                    None,
                    UnknownConstantId,   // GL_INVALID_VALUE
                    UnknownEntryPoint,   // GL_INVALID_VALUE
                    ModuleRejected,      // COMPILE_STATUS false + info log
                };

                // What a specialized module turns into: the GLSL the ordinary pipeline compiles,
                // plus the transform-feedback capture the module DECLARED, re-expressed as the
                // glTransformFeedbackVaryings request that produces the same layout.
                //
                // The re-expression is the whole design. ARB_gl_spirv makes XfbBuffer/XfbStride/
                // Offset decorations the only way a SPIR-V program declares capture, and MobileGL's
                // capture machinery - the frontend packer, DirectGLES's forwarding to the ES
                // driver, DirectVulkan's XfbCaptureDecoratePass - is driven entirely by a name
                // list. Translating the decorations into the equivalent name list (with
                // ARB_transform_feedback3's gl_NextBuffer / gl_SkipComponentsN spelling carrying
                // the buffer breaks and the gaps) hands a SPIR-V program to the machinery that
                // already exists, instead of teaching every consumer a second declaration form.
                struct SpecializedModule {
                    String glsl;
                    Vector<String> xfbVaryings;
                    GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
                };

                static Result<SpecializedModule> SpecializeAndDecompileSpirvModule(
                    const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
                    const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
                    SpecializationFailure& outFailure);

                // spirv-val over an application-supplied module, against the environment MobileGL
                // parses and emits under. glShaderBinary is where a malformed module has to be
                // caught: past it the words reach SPIRV-Cross, which is not a validator.
                static Result<void> ValidateSpirvModule(const Vector<Uint32>& spirv);

                // Parses one trivial shader in each configuration the production path can
                // reach, on the calling thread, so the built-in symbol tables those
                // configurations need are already cached before any worker asks for one.
                //
                // Why it matters: glslang builds a built-in TSymbolTable per distinct
                // (version, spvVersion, profile, source) combination, and does it under a
                // process-wide lock held for the whole build. Without this, the first
                // parallel compiles of a shaderpack load all pile up behind that lock and
                // show no speedup at all - which is easy to misread as asynchronous
                // compilation not working. Call once, from the GL thread, right after
                // glslang::InitializeProcess(). Idempotent and cheap on repeat.
                //
                // Only worth its cost when compiles can actually run in parallel, so the GL
                // frontend calls it only when asynchronous compilation is enabled: a
                // synchronous build would pay for three throwaway parses at every
                // eglInitialize to prewarm tables the first real compile builds anyway.
                static void PrewarmBuiltins();
                // Clears the "already prewarmed" latch. MUST be called wherever
                // glslang::FinalizeProcess() is, and for the same reason: finalizing deletes
                // the cached built-in tables the latch is asserting the existence of. Without
                // it, the second eglInitialize of a process comes back up unwarmed and with
                // no way left to warm it.
                static void ResetPrewarmLatch();

                // Validation is an explicit immutable option of each compiler operation. The
                // program-link task snapshots MOBILEGL_ENABLE_SPIRV_VALIDATION before it can run
                // on a worker; standalone callers pass true directly. A failure logs the VUID and
                // bumps the latch below WITHOUT changing a wrapper's return value, so validating
                // and shipping configurations preserve identical rendering control flow.

                // Makes validator table lifetime safe before an external final-module validator
                // runs. This has no configuration state; callers invoke it only for an enabled
                // task-local validation option.
                static void PrepareSpirvValidation();

                // The test-lane enforcement signal: total validation failures observed this
                // process. Tests snapshot it, run the operation under scrutiny, and assert
                // on the delta. NoteSpirvValidationFailure is for validation done outside
                // this file (ProgramFactory::ValidateTransformedSpirv); it returns the new
                // total.
                static Uint64 SpirvValidationFailureCount();
                static Uint64 NoteSpirvValidationFailure();

                // True when the module declares any buffer-backed image type - an OpTypeImage with
                // Dim = Buffer. That is the samplerBuffer / isamplerBuffer / usamplerBuffer
                // family and equally the imageBuffer / iimageBuffer / uimageBuffer one: SPIRV-Cross
                // requires GL_EXT_texture_buffer for both, from the same branch, so both are
                // uncompilable on a driver without buffer textures and both belong here.
                // DirectGLES asks before handing the transpiled ESSL to the driver: buffer
                // textures are core in the OpenGL 3.1+ context MobileGL advertises but need
                // ES 3.2 or EXT/OES_texture_buffer on the host, and on a driver without them
                // SPIRV-Cross's `#extension ... : require` makes the shader uncompilable. The
                // check exists so that failure can be reported as the missing capability it is,
                // naming the shader, rather than as a driver info log nobody sees.
                static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
                // Does this module carry the Xfb execution mode - i.e. would a
                // vkCmdBeginTransformFeedbackEXT against a pipeline whose last pre-rasterization
                // stage is this module satisfy VUID-vkCmdBeginTransformFeedbackEXT-None-04128?
                // Asked of the FINAL bytes, so it answers for whatever the backend transform
                // chain actually produced rather than for what it was asked to produce.
                static Bool ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv);
                // Does this module declare TessellationPointSize or GeometryPointSize - i.e. does
                // it need VkPhysicalDeviceFeatures::shaderTessellationAndGeometryPointSize before
                // a pipeline built from it is legal usage (VUID-RuntimeSpirv-PointSize-06439)?
                // glslang emits either capability from any access to the PointSize built-in in a
                // tessellation or geometry stage, which desktop GL treats as an ordinary
                // per-vertex output, so a program that is perfectly legal in GL can need a Vulkan
                // feature the device does not have. Callers only ask when the feature is OFF, so
                // the module parse costs nothing on a device that has it.
                static Bool ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv);

                // ---- gl_PointSize demotion for devices without the capability above ----
                // The name of the demoted program's LAST capture-capable stage's point-size
                // carrier. It is the contract three parties meet at: the demotion pass names
                // the variable, DirectVulkan's XfbCaptureDecoratePass binds a "gl_PointSize"
                // capture to it instead of mirroring the (no longer accessed) built-in, and
                // DirectGLES respells the driver-side glTransformFeedbackVaryings request
                // with it. Deliberately NOT containing the substring "gl_PointSize":
                // DirectGLES's extension-request gate is a text search for that token over
                // the emitted ESSL, and a carrier name embedding it would re-arm the decline
                // this demotion exists to retire.
                static constexpr const char* POINT_SIZE_CAPTURE_CARRIER_NAME = "mg_PointSizeCapture";

                // What the program-scoped demotion left behind. `demoted` false with an empty
                // detail means the program never needed it (no tessellation/geometry stage
                // accesses the built-in, or the device hosts it); false WITH a detail means a
                // module shape the pass cannot express - the modules are byte-identical and
                // the existing decline paths (Espryt's missing-extension compile failure,
                // Magma's pointSizeCapabilityUnsupported refusal) stay in charge of it.
                struct PointSizeDemotionOutcome {
                    Bool demoted = false;
                    String declineDetail;
                };

                // Demotes gl_PointSize across a WHOLE program's pre-rasterization chain into
                // ordinary float varyings at one shared free location, so a device that
                // advertises neither ES tessellation/geometry_point_size extension nor
                // Vulkan's shaderTessellationAndGeometryPointSize can still run programs
                // whose tessellation/geometry stages merely CARRY the value (transform
                // feedback and gl_in[].gl_PointSize reads). Runs after
                // SanitizeAndOptimizeBinary, on the final shared modules both backends
                // consume, and is atomic per program: every stage is rewritten or none is,
                // because a consumer whose producer kept the built-in would read garbage.
                // `demoteTessellation` / `demoteGeometry` are the env verdicts (the device
                // LACKS that capability); the per-program half of the decision - whether any
                // module actually declares TessellationPointSize / GeometryPointSize - is
                // probed here. `captureRequestsPointSize` forces the capture-capable last
                // stage to declare its carrier even when it never writes the built-in, so a
                // by-name capture always has something to bind to. Returns false only when
                // the optimizer itself failed (modules untouched); a shape decline is
                // reported through `outcome` and also leaves the modules untouched. See
                // DemotePointSizePass for the per-module rewrite and its honest residue.
                static Bool DemoteTessellationGeometryPointSizeForProgram(
                    Vector<Vector<Uint32>>& modules, const Vector<GLenum>& shaderTypes,
                    Bool demoteTessellation, Bool demoteGeometry, Bool captureRequestsPointSize,
                    PointSizeDemotionOutcome& outcome, bool validateOutput = true,
                    bool enableSpirvValidation = false);

                // True when the module still declares a 64-bit float type. After
                // SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
                // module (see its header for the two operations that make it decline), which is
                // what the backends report: no mobile driver can build such a module.
                static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);

                // True when the module is a VERTEX stage that declares a 64-bit float INPUT
                // variable - `in double`, `in dvec2`, `in dmat3` and so on.
                //
                // Asked only on a backend with native fp64, and it is what keeps that backend's
                // vertex path consistent. No backend here can FETCH 64 bits (VK_FORMAT_R64*_SFLOAT
                // is optional and lavapipe advertises none of them), and the format is chosen from
                // the VAO attribute, which does not know what the shader declared - so a module
                // that keeps a Float64 input would be fed a narrowed float32 stream, or a packed
                // uint pair with no matching format. Such a module is demoted WHOLE instead, which
                // is exactly what every other backend does to it.
                static Bool ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv);

                // True when the module declares an Input variable carrying a Location - i.e. a
                // user-defined varying or a per-patch input, as opposed to a built-in.
                //
                // Asked of a TESSELLATION EVALUATION stage that has no control stage, to decide
                // whether the pass-through control stage GL 4.6 core 11.2.2 describes can stand
                // in for the missing one. That stage forwards gl_Position and nothing else, so a
                // located input - which the vertex stage feeds today and which would stop
                // arriving once a control stage sat in between - means the program has to be
                // declined rather than fed an undefined varying. Same rule, same reasoning, as
                // DirectVulkan's ReflectPassthroughTessControlNeed, which asks SPIRV-Reflect the
                // identical question for the identical decision.
                static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
            };

            // The explicit layout(location = N) qualifiers this shader's DEFAULT-BLOCK uniforms
            // declared, keyed the way glslang's own reflection will later spell them.
            //
            // They cannot be read back off the parsed module, and that is not an oversight of
            // this function: MobileGL parses every shader as a Vulkan client under relaxed
            // rules, which sweeps plain uniforms into MGL_GLOBAL_UBO - where a location
            // qualifier has no meaning - and DROPS the qualifier on the way past
            // (ParseHelper.cpp vkRelaxedRemapUniformVariable). What this reads is the snapshot
            // glslang takes at that exact site, handed over through TIntermediate; the GL
            // location assigner in ProgramLinkTask::DoReflection is the only party left that
            // can honour the number.
            //
            // Keyed by declared name (no "[0]" suffix), plus one synthesized key per outer
            // index of an array-of-arrays - see the note in the implementation for why
            // reflection needs those spelled out. A uniform declared in several stages must
            // agree, which the caller enforces across stages.
            UnorderedMap<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader);
        } // namespace ShaderTranspiler
    } // namespace MG_Util
} // namespace MobileGL
