// MobileGL - MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header #define SPV_ENABLE_UTILITY_CODE #include "glslang/SPIRV/spirv.hpp11" #undef SPV_ENABLE_UTILITY_CODE #include "ShaderCompiler.h" #include #include #include #include "SpirvPasses/EliminateFloatEqualsZeroPass.h" #include "SpirvPasses/FlattenInterfaceStructPass.h" #include "SpirvPasses/RenameSamplerFunctionParameterPass.h" #include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h" #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" #include "SpirvPasses/DecoratePositionInvariantPass.h" #include "SpirvPasses/DemoteFloat64Pass.h" #include "SpirvPasses/FlattenFloat64StorageBlockPass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/LowerViewportIndexPass.h" #include "SpirvPasses/PackDoubleVertexInputsPass.h" #include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h" #include "SpirvPasses/UniquifyIoBlockNamesPass.h" #include "SpirvPasses/SplitArrayVertexInputsPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/ZeroBaseVertexPass.h" #include "SpirvPasses/LowerPixelCenterIntegerPass.h" #include "SpirvPasses/ZeroLayerOutputPass.h" #include "SpirvPasses/FullCoverageSampleMaskPass.h" #include "SpirvPasses/DeriveNumSubgroupsPass.h" #include "SpirvPasses/EmulateSubgroupsPass.h" #include "SpirvPasses/FixIterationRPBarrierPass.h" #include "SpirvPasses/FixIterationRPSubgroupScratchPass.h" #include "SpirvPasses/NormalizeRectCoordinatesPass.h" #include "SpirvPasses/DeduplicateImageTypesPass.h" #include "SpirvPasses/Lower1DArrayImagesPass.h" #include "SpirvPasses/Lower1DSampledImagesPass.h" #include "SpirvPasses/BakeImageFormatsPass.h" #include "SpirvPasses/WidenImageFormatsPass.h" #include "SpirvPasses/ClampMultisampleFetchPass.h" #include "SpirvPasses/PrivateToEntryLocalPass.h" #include "SpirvPasses/StripUniformLocationsPass.h" #include "SpirvPasses/StripIoBlockLocationsPass.h" #include "SpirvPasses/DeferGeometryEmitsPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h" #include "SpirvPasses/LegalizeFragmentOutputIndexPass.h" #include "SpirvPasses/LegalizeResourceArrayIndexPass.h" #include "SpirvPasses/FlattenAtomicCounterBlockPass.h" #include "SpirvPasses/DemotePointSizePass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" #include "source/opt/build_module.h" #include "source/opt/ir_context.h" #include "ShaderSourceProcessor.h" #include #include #include #include #include #include #include #include namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { // Above every plausible GL_MAX_TESS_GEN_LEVEL (the GL core minimum is 64), so it lands on the // same clamped result the device's own maximum would. +inf has to reach the tessellator as // "as finely as possible", not as "discard". static constexpr const char* kClampedHighTessLevelLiteral = "65536.0"; String TessellationLevelLiteral(Float value) { // GL leaves a NaN level unspecified; 0.0 is the safe reading, and unlike "nan" it compiles. if (std::isnan(value)) return "0.0"; // -inf is <= 0 and discards the patch, exactly like 0.0. +inf clamps to the maximum. if (std::isinf(value)) return value > 0.0f ? kClampedHighTessLevelLiteral : "0.0"; // Shortest round-trip, not a fixed six decimals: "{:.6f}" renders every level below ~5e-7 // as "0.000000", which turns a positive level GL would clamp to 1 into a discarded patch. String text = std::format("{}", value); // ...but shortest round-trip spells an integral value as a bare digit sequence, which GLSL // reads as an INT literal, so the decimal point has to be put back when nothing else marks // the literal as floating point. if (text.find('.') == String::npos && text.find('e') == String::npos && text.find('E') == String::npos) { text += ".0"; } return text; } // `env` is the compile-time backend snapshot; null means "resolve from the live // backend", which is what the standalone/test entry points do. The pipeline always // passes one, so a worker never reaches pActiveBackendObject through here. TBuiltInResource BuildTBuiltInResource(const CompileEnv* env) { TBuiltInResource Resources{}; Resources.maxLights = 32; Resources.maxClipPlanes = 6; Resources.maxTextureUnits = 32; Resources.maxTextureCoords = 32; Resources.maxVertexUniformComponents = MAX_VERTEX_UNIFORM_COMPONENTS; Resources.maxVaryingFloats = MAX_VARYING_COMPONENTS; Resources.maxFragmentUniformComponents = 4096; Resources.maxVertexUniformVectors = MAX_VERTEX_UNIFORM_VECTORS; Resources.maxVaryingVectors = MAX_VARYING_VECTORS; Resources.maxFragmentUniformVectors = 256; Resources.maxVertexOutputVectors = 16; Resources.maxFragmentInputVectors = 15; Resources.minProgramTexelOffset = -8; Resources.maxProgramTexelOffset = 7; Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS; Resources.maxComputeTextureImageUnits = 16; Resources.maxComputeImageUniforms = 8; Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS; Resources.maxVertexOutputComponents = 64; Resources.maxGeometryInputComponents = 64; Resources.maxGeometryOutputComponents = 128; Resources.maxFragmentInputComponents = 128; Resources.maxImageUnits = 8; Resources.maxImageSamples = 0; Resources.maxVertexImageUniforms = 0; Resources.maxTessControlImageUniforms = 0; Resources.maxTessEvaluationImageUniforms = 0; Resources.maxGeometryImageUniforms = 0; Resources.maxFragmentImageUniforms = 8; Resources.maxCombinedImageUniforms = 8; Resources.maxGeometryTextureImageUnits = 16; Resources.maxGeometryOutputVertices = 256; Resources.maxGeometryTotalOutputComponents = 1024; Resources.maxGeometryUniformComponents = 1024; Resources.maxGeometryVaryingComponents = 64; // The tessellation block is shared with glGetIntegerv through Types.h; see the // "Never move one of these without the other" note there. Resources.maxTessControlInputComponents = MAX_TESS_CONTROL_INPUT_COMPONENTS; Resources.maxTessControlOutputComponents = MAX_TESS_CONTROL_OUTPUT_COMPONENTS; Resources.maxTessControlTextureImageUnits = MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS; Resources.maxTessControlUniformComponents = MAX_TESS_CONTROL_UNIFORM_COMPONENTS; Resources.maxTessControlTotalOutputComponents = MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS; Resources.maxTessEvaluationInputComponents = MAX_TESS_EVALUATION_INPUT_COMPONENTS; Resources.maxTessEvaluationOutputComponents = MAX_TESS_EVALUATION_OUTPUT_COMPONENTS; Resources.maxTessEvaluationTextureImageUnits = MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS; Resources.maxTessEvaluationUniformComponents = MAX_TESS_EVALUATION_UNIFORM_COMPONENTS; Resources.maxTessPatchComponents = MAX_TESS_PATCH_COMPONENTS; Resources.maxPatchVertices = 32; Resources.maxTessGenLevel = 64; Resources.maxViewports = 16; // Every atomic-counter limit below is the one glGetIntegerv answers; the shared // constants in Types.h are what keeps the two sides from drifting apart again. // gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these // (Initialize.cpp), and the binding count is also the ceiling glslang checks a // `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter // outside binding 0 failed to compile. // The per-stage and combined counter counts are env-derived and set further down. Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS; Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE; Resources.maxTransformFeedbackBuffers = 4; Resources.maxTransformFeedbackInterleavedComponents = 64; Resources.maxMeshOutputVerticesNV = 256; Resources.maxMeshOutputPrimitivesNV = 512; Resources.maxMeshWorkGroupSizeX_NV = 32; Resources.maxMeshWorkGroupSizeY_NV = 1; Resources.maxMeshWorkGroupSizeZ_NV = 1; Resources.maxTaskWorkGroupSizeX_NV = 32; Resources.maxTaskWorkGroupSizeY_NV = 1; Resources.maxTaskWorkGroupSizeZ_NV = 1; Resources.maxMeshViewCountNV = 4; // Resource checking must describe the same backend contract exposed through // glGetIntegerv. Keeping this copy local also avoids racing on a process-global // TBuiltInResource when Iris compiles shaders concurrently. // // MEMO-HAZARD RULE FOR THIS BLOCK. Everything below is an env-derived value that // glslang enforces at parse AND expands into a built-in constant, so every one of // them can change the SPIR-V a module generates. EVERY LINE BELOW MUST BE HASHED // BY ComputeFrontendCompileEnvFingerprint(), which is the L1 shader-translation // memo's environment key - adding a read here without adding it there is a silent // miscompile, not a slow path. See the classification on // CompileEnv::frontendFingerprint. const MG_Backend::DynamicBackendParameters fallbackParameters{}; const auto& activeBackend = MG_Backend::pActiveBackendObject; const auto& dynamicParameters = env ? env->params : (activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters); Resources.maxImageUnits = dynamicParameters.MaxImageUnits; Resources.maxCombinedImageUnitsAndFragmentOutputs = dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; // GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES and // GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS are the SAME token (0x8F39), so // the two glslang fields have to carry the same value: glGetIntegerv answers this // expression while gl_MaxCombinedShaderOutputResources expanded from a stale // literal 8, and the CTS compares the two directly. Resources.maxCombinedShaderOutputResources = Resources.maxCombinedImageUnitsAndFragmentOutputs; Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms; Resources.maxTessControlImageUniforms = dynamicParameters.MaxTessControlImageUniforms; Resources.maxTessEvaluationImageUniforms = dynamicParameters.MaxTessEvaluationImageUniforms; Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms; Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits; // The texture-image-unit family and the draw-buffer count. These were stock // glslang defaults (32 / 32 / 80 / 32) that had nothing to do with what // glGetIntegerv answers off the same backend, and the divergence is a live // correctness bug rather than a reporting one: gl_MaxDrawBuffers = 32 makes // glslang ACCEPT a fragment output at location 8..31 that the runtime cannot // bind, and gl_MaxCombinedTextureImageUnits = 80 under-reports a device that // really has 96. Resources.maxTextureImageUnits = dynamicParameters.MaxTextureImageUnits; Resources.maxVertexTextureImageUnits = dynamicParameters.MaxVertexTextureImageUnits; Resources.maxCombinedTextureImageUnits = dynamicParameters.MaxCombinedTextureImageUnits; Resources.maxDrawBuffers = dynamicParameters.MaxDrawBuffers; // The same number glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) reports and the same one // reflection records vertex inputs against - see ResolveMaxVertexAttribs. Resources.maxVertexAttribs = ResolveMaxVertexAttribs( env ? env->HasBackend() : (activeBackend != nullptr), dynamicParameters.MaxVertexAttribs); // gl_MaxSamples, floored exactly as GL_Getter::GetAdvertisedMaxSamples floors // GL_MAX_SAMPLES. It also sizes gl_SampleMask[] / gl_SampleMaskIn[]. Resources.maxSamples = std::max(dynamicParameters.MaxSamples, MIN_ADVERTISED_MAX_SAMPLES); // Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for // i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the // same number, so tracking the backend limit is what turns "the program links, // the backend's shader compile fails somewhere the frontend never surfaces, and // the draw renders nothing" into an honest glCompileShader error with a log. It is // also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances // agree, which KHR-GLxx.clip_distance.coverage compares directly. Resources.maxClipDistances = dynamicParameters.MaxClipDistances; // The cull pair, for the same reason and with a sharper edge: cull distance // discards the WHOLE primitive, so a shader that gets to declare gl_CullDistance // on a backend that cannot host one does not render subtly wrong pixels, it // renders nothing at all. These were literal 8s that no backend was ever asked // about; a backend without cull distances now reports 0 and glslang rejects the // declaration with a diagnostic the application can read. Resources.maxCullDistances = dynamicParameters.MaxCullDistances; Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances; // The per-stage and combined atomic-counter limits, from the same derivation // glGetIntegerv answers with (ResolveAtomicCounterLimits): a stage's counter // buffers are lowered storage blocks, so they follow the backend's per-stage // storage-block budget, and gl_Max*AtomicCounter* has to expand to that number. { const AtomicCounterLimits counterLimits = ResolveAtomicCounterLimits(dynamicParameters); Resources.maxVertexAtomicCounters = counterLimits.stageCounters[0]; Resources.maxTessControlAtomicCounters = counterLimits.stageCounters[1]; Resources.maxTessEvaluationAtomicCounters = counterLimits.stageCounters[2]; Resources.maxGeometryAtomicCounters = counterLimits.stageCounters[3]; Resources.maxFragmentAtomicCounters = counterLimits.stageCounters[4]; Resources.maxComputeAtomicCounters = counterLimits.stageCounters[5]; Resources.maxCombinedAtomicCounters = counterLimits.combinedCounters; Resources.maxVertexAtomicCounterBuffers = counterLimits.stageBuffers[0]; Resources.maxTessControlAtomicCounterBuffers = counterLimits.stageBuffers[1]; Resources.maxTessEvaluationAtomicCounterBuffers = counterLimits.stageBuffers[2]; Resources.maxGeometryAtomicCounterBuffers = counterLimits.stageBuffers[3]; Resources.maxFragmentAtomicCounterBuffers = counterLimits.stageBuffers[4]; Resources.maxComputeAtomicCounterBuffers = counterLimits.stageBuffers[5]; Resources.maxCombinedAtomicCounterBuffers = counterLimits.combinedBuffers; } // The compute work-group limits are the env's, not the backend parameters': they // are the only ones that come from a REAL indexed driver query, which // CaptureCompileEnv already issued once on the GL thread and floored at the core // minimum exactly as GL_Getter does. Reading the same snapshot here is what makes // gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction // (KHR-GL43.compute_shader.max compares them); the z component was 1024 here // against the 64 every ES driver reports. A null env is the standalone/test entry // point, which has no context to have queried one - the core minimums stand, which // is what a default-constructed CompileEnv carries anyway. const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE; const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT; Resources.maxComputeWorkGroupSizeX = static_cast(maxWorkGroupSize[0]); Resources.maxComputeWorkGroupSizeY = static_cast(maxWorkGroupSize[1]); Resources.maxComputeWorkGroupSizeZ = static_cast(maxWorkGroupSize[2]); Resources.maxComputeWorkGroupCountX = static_cast(maxWorkGroupCount[0]); Resources.maxComputeWorkGroupCountY = static_cast(maxWorkGroupCount[1]); Resources.maxComputeWorkGroupCountZ = static_cast(maxWorkGroupCount[2]); Resources.limits.nonInductiveForLoops = true; Resources.limits.whileLoops = true; Resources.limits.doWhileLoops = true; Resources.limits.generalUniformIndexing = true; Resources.limits.generalAttributeMatrixVectorIndexing = true; Resources.limits.generalVaryingIndexing = true; Resources.limits.generalSamplerIndexing = true; Resources.limits.generalVariableIndexing = true; Resources.limits.generalConstantMatrixVectorIndexing = true; return Resources; } // One parse attempt. A glslang::TShader cannot be re-parsed, so a retry has to build a // fresh one with byte-identical setup - hence a single factored body rather than two // copies that could drift apart. static Result> ParseShaderSource(EShLanguage lang, GLenum shaderType, const String& source, Flags flags, const CompileEnv* env) { SharedPtr res; auto& tshader = res; tshader = MakeShared(lang); // setStrings gets no length array, so it relies on NUL termination: source must be an // owning buffer that outlives parse(), never a StringView's substring. const char* src[] = {source.c_str()}; tshader->setStrings(src, 1); tshader->setNanMinMaxClamp(true); tshader->setInvertY(true); // The custom preamble is glslang string -1, which CPPdefine exempts from the // "names beginning with GL_ can't be (un)defined" rule - so it is the only place // an ES source's extension macros can be put back after PreprocessShaderSource // rewrote its #version to desktop and cost it glslang's ES preamble. Empty for // every source that was not rewritten from ES, which is almost all of them. // // setPreamble stores the POINTER, so the buffer has to outlive parse() below. const String preamble = String("#undef VULKAN\n") + CollectEsPreambleMacroDefines(source); tshader->setPreamble(preamble.c_str()); if (flags & ShaderCompileBits::CompileForOpenGL) { tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450); tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450); tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3); } else { tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450); // MobileGL runtime currently creates Vulkan 1.1 instance/device on Android path, // so generated SPIR-V must not exceed SPIR-V 1.3. tshader->setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_1); tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3); tshader->setEnvInputVulkanRulesRelaxed(); // using EXT_vulkan_glsl_relaxed for gl_VertexID and // gl_InstanceID? } tshader->setAutoMapLocations(true); tshader->setAutoMapBindings(true); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); auto resources = BuildTBuiltInResource(env); if (!tshader->parse(&resources, 460, ECoreProfile, /*forceDefaultVersionAndProfile: */ false, /*forwardCompatible: */ true, EShMsgDefault)) { ResultInfo r; r.log += "Error: [glslang] Cannot compile " + ConvertGLEnumToString(shaderType) + ":\n" + std::string(tshader->getInfoLog()); r.errc = -2; return std::unexpected(r); } return res; } namespace { // The first variable index into a uniform or shader storage block ARRAY, if any. class BlockArrayVariableIndexFinder final : public glslang::TIntermTraverser { public: const glslang::TIntermBinary* found = nullptr; bool visitBinary(glslang::TVisit, glslang::TIntermBinary* node) override { if (found) return false; if (node->getOp() != glslang::EOpIndexIndirect) return true; const glslang::TType& base = node->getLeft()->getType(); const glslang::TStorageQualifier storage = base.getQualifier().storage; if (base.getBasicType() == glslang::EbtBlock && base.isArray() && (storage == glslang::EvqBuffer || storage == glslang::EvqUniform)) { found = node; return false; } return true; } }; // ESSL 3.10 4.3.9: "All indices used to index a uniform or shader storage block array // must be constant integral expressions." ESSL 3.20 and GL_EXT/OES_gpu_shader5 relax // it to dynamically uniform; desktop GLSL has no such rule at all. The rewrite to // "#version 460 core" lost it, so it is put back here for the sources that declared // an `es` version - and only those. Returns the info-log line, or empty. String FindEsBlockArrayIndexViolation(const glslang::TShader& shader, const String& source) { const std::optional origin = FindEsOrigin(source); if (!origin || origin->version >= 320 || origin->gpuShader5) return {}; glslang::TIntermediate* intermediate = shader.getIntermediate(); if (intermediate == nullptr || intermediate->getTreeRoot() == nullptr) return {}; BlockArrayVariableIndexFinder finder; intermediate->getTreeRoot()->traverse(&finder); if (!finder.found) return {}; const glslang::TType& base = finder.found->getLeft()->getType(); return std::format("ERROR: 0:{}: '[' : a {} block array must be indexed with a constant " "integral expression in ESSL {} without GL_EXT_gpu_shader5\n", finder.found->getLoc().line, base.getQualifier().storage == glslang::EvqBuffer ? "shader storage" : "uniform", origin->version); } } // namespace Result> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) { auto shaderType = attrib.shaderType; auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType); if (lang == EShLanguage::EShLangCount) { ResultInfo r; r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType); r.errc = -1; return std::unexpected(r); } const String source(attrib.sourceStr); auto result = ParseShaderSource(lang, shaderType, source, attrib.flags, attrib.env); if (result) { // Part of the parse verdict, so an L1c hit and ClaimParsedShader's re-parse reach // the same answer as this parse did. if (String violation = FindEsBlockArrayIndexViolation(*result.value(), source); !violation.empty()) { ResultInfo r; r.log = "Error: [glslang] Cannot compile " + ConvertGLEnumToString(shaderType) + ":\n" + violation; r.errc = -2; return std::unexpected(r); } return result; } // Legacy desktop sources are normalized to "#version 330 core" (with a marker on the // directive), which parses under stricter rules than the 460 they used to be forced // to: a shader declaring 110-150 while using e.g. layout(binding=...) without the // matching #extension line compiles on real drivers but is rejected here. Retry once // at 460 before reporting failure; a genuinely broken shader fails both attempts and // keeps its original diagnostics. Application-declared 330+ sources carry no marker // and keep their declared version's strict rules (the GL CTS negative-compile cases // depend on that). String retrySource = source; if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) { return result; } auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags, attrib.env); if (!retryResult) return result; MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460", ConvertGLEnumToString(shaderType).c_str()); return retryResult; } // Namespace-level rather than a function-local static, because it has to be // CLEARABLE: what PrewarmBuiltins latches is not a property of this process, it is // a property of the built-in symbol tables glslang currently holds, and // glslang::FinalizeProcess() deletes those. A function-local latch survived the // teardown that invalidated it, so an Initialize -> Destroy -> Initialize cycle // came back up with the tables gone and the prewarm skipped - which is exactly the // serialized-first-parse stall this function exists to prevent, only now // unfixable for the rest of the process. Reset it from DestroyImpl. namespace { Bool g_builtinsPrewarmed = false; } // namespace void ShaderCompiler::ResetPrewarmLatch() { g_builtinsPrewarmed = false; } void ShaderCompiler::PrewarmBuiltins() { if (g_builtinsPrewarmed) return; g_builtinsPrewarmed = true; // One vertex and one fragment shader is enough: the built-in table is cached // per (version, spvVersion, profile, source), not per stage language, and // both configurations CompileShader can reach - the declared-460 path and // the retargeted-legacy path - resolve to the same combination here because // ParseShaderSource always passes 460/ECoreProfile as the default. Parsing // both anyway costs microseconds and keeps this honest if that ever changes. static constexpr const char* kPrewarmVertexSource = "#version 460\nvoid main() { gl_Position = vec4(0.0); }\n"; static constexpr const char* kPrewarmFragmentSource = "#version 460\nlayout(location = 0) out vec4 c;\nvoid main() { c = vec4(0.0); }\n"; static constexpr const char* kPrewarmLegacyVertexSource = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n"; const CompileEnv& env = *GetDefaultCompileEnv(); for (const auto& [type, source] : {std::pair{GL_VERTEX_SHADER, kPrewarmVertexSource}, std::pair{GL_FRAGMENT_SHADER, kPrewarmFragmentSource}, std::pair{GL_VERTEX_SHADER, kPrewarmLegacyVertexSource}}) { ShaderAttrib attrib{.shaderType = static_cast(type), .sourceStr = source, .flags = 0, .env = &env}; // The result is deliberately discarded: the value is the symbol table // glslang cached as a side effect. A failure here is not fatal - it just // means the first real compile pays for the table, exactly as before. (void)CompileShader(attrib); } // The parses above left this thread's glslang allocator pointing at the last // TShader's pool, and that TShader is about to be destroyed with it. glslang::SetThreadPoolAllocator(nullptr); } namespace { // glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER // outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes // "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The // linker resolves such a name by stripping the single trailing "[0]", so it looks // up "u[1]" - a key the root entry alone cannot answer, and the whole declaration // silently loses its explicit location. // // Emit those pre-flattened keys next to the root, so the result is // order-independent: each carries the location its own element starts at (element // i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot // contain brackets, so a synthesized key never collides with a real uniform name, // and a 1-D array needs none of this - stripping "[0]" already reaches the root. void RecordArrayOfArraysElementLocations(const String& name, const std::vector& dimensions, const long long baseLocation, UnorderedMap& locations) { if (dimensions.size() < 2) return; // A pathological declaration must not be able to blow up the map; past the cap // only the root entry stands, which is what every case used to get. constexpr long long kMaxSynthesizedKeys = 4096; const long long innerSpan = dimensions.back(); const SizeT outerDimensions = dimensions.size() - 1; long long elementCount = 1; for (SizeT d = 0; d < outerDimensions; ++d) { elementCount *= dimensions[d]; if (elementCount > kMaxSynthesizedKeys) return; } for (long long element = 0; element < elementCount; ++element) { String key = name; long long remainder = element; for (SizeT d = 0; d < outerDimensions; ++d) { long long stride = 1; for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner]; key += "[" + std::to_string(remainder / stride) + "]"; remainder %= stride; } locations.emplace(key, static_cast(std::min(baseLocation + element * innerSpan, static_cast(INT_MAX / 2)))); } } } // namespace Bool PreprocessForInspection(GLenum shaderType, const String& source, String& out) { const EShLanguage lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType); if (lang == EShLanguage::EShLangCount) return false; Bool ok = false; { glslang::TShader shader(lang); const char* strings[] = {source.c_str()}; shader.setStrings(strings, 1); const String preamble = String("#undef VULKAN\n") + CollectEsPreambleMacroDefines(source); shader.setPreamble(preamble.c_str()); shader.setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450); shader.setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450); shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3); const auto resources = BuildTBuiltInResource(&*GetDefaultCompileEnv()); glslang::TShader::ForbidIncluder includer; std::string text; ok = shader.preprocess(&resources, 460, ECoreProfile, false, true, EShMsgDefault, &text, includer); if (ok) out = text; } // The shader's pool is gone with it; leave no dangling thread allocator behind. glslang::SetThreadPoolAllocator(nullptr); return ok; } UnorderedMap CollectExplicitUniformLocations(const glslang::TShader& shader) { UnorderedMap locations; const glslang::TIntermediate* intermediate = shader.getIntermediate(); if (intermediate == nullptr) return locations; // Half one: the uniforms the relaxed remap swallowed, out of the snapshot it // takes on the way past. for (const glslang::TIntermediate::TUniformLocation& record : intermediate->getUniformLocations()) { if (record.location < 0) continue; // Keep the first sighting. Two records for one name mean the parser saw the // declaration twice, and the first is the one the symbol table kept. locations.emplace(record.name, record.location); RecordArrayOfArraysElementLocations(record.name, record.arraySizes, record.location, locations); } // Half two: the OPAQUE uniforms, which the remap never touches (the guard in // vkRelaxedRemapUniformVariable admits only types containing something // non-opaque, atomic_uint, or a sampler inside a struct) and which therefore // still carry their qualifier here. // // They belong in the same map even though reflection could also answer for them, // and the distinction is not cosmetic: this map is what marks a location as // SOURCE-EXPLICIT, i.e. API contract under ARB_explicit_uniform_location. A // location that only reaches DoReflection through glslang's own layoutLocation() // is treated as implementation-chosen and quietly moved on a collision, which is // the wrong answer for one the shader declared. // // Read BEFORE any link: mapIO writes its own choices into these same qualifiers // (iomapper.cpp:240), so this is only truthful while the shader is unlinked - // which is exactly where ShaderCompileTask calls it. const glslang::TIntermAggregate* linkerObjects = intermediate->findLinkerObjects(); if (linkerObjects == nullptr) return locations; for (TIntermNode* node : linkerObjects->getSequence()) { const glslang::TIntermSymbol* symbol = node ? node->getAsSymbolNode() : nullptr; if (symbol == nullptr) continue; const glslang::TType& type = symbol->getType(); const glslang::TQualifier& qualifier = type.getQualifier(); if (qualifier.storage != glslang::EvqUniform || !qualifier.hasLocation()) continue; // A BLOCK has no glGetUniformLocation of its own, and its members are // addressed through the block. Only loose uniforms take locations. if (type.getBasicType() == glslang::EbtBlock || type.isBuiltIn()) continue; std::vector arraySizes; if (type.isArray() && type.getArraySizes() != nullptr) { const glslang::TArraySizes& sizes = *type.getArraySizes(); for (int dim = 0; dim < sizes.getNumDims(); ++dim) { arraySizes.push_back(sizes.getDimSize(dim)); } } const String name = symbol->getAccessName().c_str(); const Int location = static_cast(qualifier.layoutLocation); locations.emplace(name, location); RecordArrayOfArraysElementLocations(name, arraySizes, location, locations); } return locations; } Result> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) { SharedPtr program = MakeShared(); for (auto& s : attrib.shaders) { program->addShader(s.get()); } if (!program->link(EShMsgDefault)) { ResultInfo r; r.log = "Error: [glslang] Cannot link the program:\n" + std::string(program->getInfoLog()); r.errc = -3; return std::unexpected(r); } for (const auto& [name, loc] : attrib.explicitVertexInLocations) { MGLOG_D("%s: got explicitly set - layout(location = %d) %s;", __func__, loc, name.c_str()); } // UniquePtr resolver; UniquePtr resolver; for (unsigned stage = 0; stage < EShLangCount; stage++) { if (program->getIntermediate((EShLanguage)stage) == nullptr) continue; resolver = MakeUnique(*program, (EShLanguage)stage, attrib.explicitVertexInLocations, attrib.explicitFragmentOutLocations, attrib.explicitFragmentOutIndices, attrib.explicitOpaqueUniformBindings, attrib.storageBlocksWithoutBinding, attrib.uniformBlocksWithoutBinding, &attrib.resourceBindingLimits, attrib.resourceBindingViolation); break; } auto ioMapper = UniquePtr(glslang::GetGlslIoMapper()); const bool mapped = program->mapIO(resolver.get(), ioMapper.get()); // The binding-range verdict is read BEFORE mapIO's own outcome, and unconditionally: // the resolver fills it during the collect phase, which runs whether or not doMap() // later succeeds, and a shader that names an out-of-range binding is rejected for // THAT reason no matter what else the mapper made of it. Reporting the mapper's // generic failure instead would hand the application an info log that says nothing // about the declaration it has to fix. if (attrib.resourceBindingViolation != nullptr && !attrib.resourceBindingViolation->empty()) { ResultInfo r; r.log = *attrib.resourceBindingViolation; r.errc = -5; return std::unexpected(r); } if (!mapped) { ResultInfo r; r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog()); r.errc = -4; return std::unexpected(r); } return program; } Result>> ShaderCompiler::GetSpirvBinaryFromProgram( const ProgramBinaryAttrib& attrib) { glslang::SpvOptions spvOptions; spvOptions.disableOptimizer = false; Vector> allSpirv; for (auto type : attrib.shaderTypes) { Vector spirv; GlslangToSpv(*attrib.program.getIntermediate(ConvertGLEnumToEShLanguage(type)), spirv, &spvOptions); allSpirv.push_back(spirv); } return allSpirv; } // Total validation failures observed this process. This latch - not the wrappers' // return values - is the test-lane signal: validation must never change what a // wrapper returns, or the validating lanes would render differently from the // shipping configuration (fail-open call sites would silently substitute an // earlier-stage module). static std::atomic g_spirvValidationFailures{0}; namespace { // spirv-tools' validator lazily constructs function-local static tables on // its first run, which on this codebase happens on a ShaderCompilePool // worker. Function-local statics are destroyed in reverse construction // order, so those tables would die BEFORE the pool's own atexit sentinel // (registered at first pool use) gets to drain the workers - and a worker // mid-Validate would then read freed memory during process exit. Pin the // order instead: force the tables into existence now, then register a // second drain handler; being registered after the tables' destructors, it // runs before them. void PinValidatorTablesForProcessExit() { static std::once_flag pinnedOnce; std::call_once(pinnedOnce, [] { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); Vector warmup; // The module is shaped to reach BOTH lazily-constructed tables in // the vendored validate_id.cpp: a type-generating operand pins // InstructionCanHaveTypeOperand's allow-set, and the OpExtInst use // of the TYPELESS %glsl import is the one path into // InstructionRequiresTypeOperand's deny-set (its call site is // guarded on a referenced def with no result type). A straight-line // module without it leaves the deny-set to be built later on a pool // worker, re-creating the exit-order hazard for that one table. if (tools.Assemble("OpCapability Shader\n" "%glsl = OpExtInstImport \"GLSL.std.450\"\n" "OpMemoryModel Logical GLSL450\n" "OpEntryPoint GLCompute %main \"main\"\n" "OpExecutionMode %main LocalSize 1 1 1\n" "%void = OpTypeVoid\n" "%fn = OpTypeFunction %void\n" "%float = OpTypeFloat 32\n" "%c = OpConstant %float 1\n" "%main = OpFunction %void None %fn\n" "%entry = OpLabel\n" "%abs = OpExtInst %float %glsl FAbs %c\n" "OpReturn\n" "OpFunctionEnd\n", &warmup)) { tools.Validate(warmup); } std::atexit(+[] { Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit(); }); }); } spvtools::MessageConsumer MakeSpirvMessageConsumer(const char* site) { return [site](spv_message_level_t level, const char* /*source*/, const spv_position_t& position, const char* message) { const char* text = message ? message : ""; switch (level) { case SPV_MSG_FATAL: case SPV_MSG_INTERNAL_ERROR: case SPV_MSG_ERROR: // Unlatched: only reachable with the validation switch armed, // and every VUID names a different defect. (Parked at MGLOG_I // until the Log.h ordering fix made E live at INFO.) MGLOG_E("[spirv] %s: %s (word index %zu)", site, text, position.index); break; default: MGLOG_D("[spirv] %s: %s", site, text); break; } }; } // Validation is decoupled from control flow on purpose: a failure logs and // bumps the latch, and the caller proceeds exactly as the shipping (non- // validating) configuration would. Tests assert on the latch delta. void ValidateOrLatch(const char* site, const Vector& binary, const bool enableSpirvValidation) { if (!enableSpirvValidation) return; PinValidatorTablesForProcessExit(); spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); tools.SetMessageConsumer(MakeSpirvMessageConsumer(site)); if (!tools.Validate(binary)) { MGLOG_E("[spirv] %s: produced a module that fails validation (failure #%llu)", site, static_cast( ShaderCompiler::NoteSpirvValidationFailure())); } } // Shared tail for every Optimizer wrapper in this file. The optimizer's own // input validator stays off even in validating lanes, for two reasons: its // failure is indistinguishable from a transform failure (Optimizer::Run // returns false before BuildModule), and the FIRST wrapper's input is // glslang output that is legitimately not Vulkan-clean yet. What gets // validated is each wrapper's OUTPUT - the only bytes a driver can ever // receive. The message consumer is installed unconditionally: without one, // spirv-tools drops pass diagnostics on the floor. bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer, const Vector& inputBinary, Vector& outputBinary, const bool validateOutput, const bool enableSpirvValidation) { spvtools::OptimizerOptions options; options.set_run_validator(false); optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site)); if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) { return false; } if (validateOutput) { ValidateOrLatch(site, outputBinary, enableSpirvValidation); } return true; } } // namespace void ShaderCompiler::PrepareSpirvValidation() { PinValidatorTablesForProcessExit(); } Uint64 ShaderCompiler::NoteSpirvValidationFailure() { return g_spirvValidationFailures.fetch_add(1, std::memory_order_relaxed) + 1; } Uint64 ShaderCompiler::SpirvValidationFailureCount() { return g_spirvValidationFailures.load(std::memory_order_relaxed); } Bool ShaderCompiler::ModuleDeclaresBufferTextureSampler(const Vector& spirv) { if (spirv.empty()) { // Early out rather than letting BuildModule reject it: an empty module is a // stage that produced no SPIR-V, which is not a capability verdict, and the // parse would push a spurious diagnostic through the message consumer first. return false; } // Callers gate this on the driver LACKING buffer textures, so the module build // here only ever happens on a degraded driver that is about to fail the compile // anyway - it is not on the healthy path. std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresBufferTextureSampler"), spirv.data(), spirv.size()); if (!context) { // Unparseable here means unusable downstream too; let the ordinary transpile // path produce the error rather than inventing a capability verdict from it. return false; } for (const spvtools::opt::Instruction& type : context->types_values()) { if (type.opcode() != spv::Op::OpTypeImage) { continue; } // OpTypeImage in-operands: Sampled Type, Dim, Depth, Arrayed, MS, Sampled, // Format. Dim is operand 1; Dim::Buffer is what samplerBuffer/isamplerBuffer/ // usamplerBuffer all lower to, whatever their sampled type - and equally what // the imageBuffer family lowers to, which is correct here because SPIRV-Cross // requires the same extension for those. The operand-count guard mirrors // NormalizeRectCoordinatesPass, which reads the same operand. if (type.NumInOperands() >= 2 && static_cast(type.GetSingleWordInOperand(1)) == spv::Dim::Buffer) { return true; } } return false; } Bool ShaderCompiler::ModuleDeclaresTransformFeedback(const Vector& spirv) { if (spirv.empty()) { return false; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresTransformFeedback"), spirv.data(), spirv.size()); if (!context) { // Unparseable is not a capture verdict; say no, which makes the caller decline // the span rather than issue transform-feedback commands against it. return false; } // The exact question VUID-vkCmdBeginTransformFeedbackEXT-None-04128 asks of the // bound pipeline's last pre-rasterization stage: was it declared with the Xfb // execution mode. Reading the execution modes rather than the TransformFeedback // capability because the capability can legally be declared by a module that has // no Xfb entry point, and the VUID is about the mode. for (const spvtools::opt::Instruction& mode : context->module()->execution_modes()) { if (mode.NumInOperands() >= 2 && static_cast(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::Xfb) { return true; } } return false; } Bool ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(const Vector& spirv) { if (spirv.empty()) { return false; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresTessellationOrGeometryPointSize"), spirv.data(), spirv.size()); if (!context) { // Unparseable is not a verdict about point size. Say no, so the caller keeps // building the program: the module is already broken for other reasons and // the diagnostics that own that failure are better placed than this one. return false; } // The CAPABILITY, not the BuiltIn decoration, because the capability is exactly // what the feature gates: a module may declare gl_PerVertex with a PointSize // member and never access it, and glslang then emits no capability // (GlslangToSpv defers it to actual use) - such a module is legal without the // feature and must not be declined. for (const spvtools::opt::Instruction& capability : context->capabilities()) { if (capability.NumInOperands() < 1) continue; const auto declared = static_cast(capability.GetSingleWordInOperand(0)); if (declared == spv::Capability::TessellationPointSize || declared == spv::Capability::GeometryPointSize) { return true; } } return false; } namespace { namespace opt_analysis = spvtools::opt::analysis; // Locations one value of `type` consumes (GL 4.6 core 11.1.2.1). Unknown // shapes OVERESTIMATE (4) rather than fail: this feeds the free-location // choice for the demoted point-size carrier, where an overestimate wastes a // couple of slots and an underestimate aliases a live varying. Uint32 ConservativeLocationSpan(const opt_analysis::Type* type) { constexpr Uint32 kUnknownSpan = 4; if (type == nullptr) return kUnknownSpan; if (type->AsFloat() != nullptr || type->AsInteger() != nullptr || type->AsBool() != nullptr) { return 1u; } if (const auto* vector = type->AsVector()) { // 64-bit INTEGER elements count exactly like 64-bit floats: // ARB_gpu_shader_int64 extends 11.1.2.1's double-precision rule // verbatim to i64/u64, and DirectVulkan advertises that extension // unconditionally - so answering "one location" for an i64vec4 would // place the carrier on the SECOND location that varying already owns, // which is the underestimate this function's header forbids. const auto* element = vector->element_type(); const auto* elementFloat = element->AsFloat(); const auto* elementInteger = element->AsInteger(); const Bool is64Bit = (elementFloat != nullptr && elementFloat->width() == 64) || (elementInteger != nullptr && elementInteger->width() == 64); return (is64Bit && vector->element_count() > 2) ? 2u : 1u; } if (const auto* matrix = type->AsMatrix()) { return ConservativeLocationSpan(matrix->element_type()) * matrix->element_count(); } if (const auto* array = type->AsArray()) { const auto& lengthWords = array->length_info().words; if (lengthWords.size() != 2 || lengthWords[0] != static_cast(opt_analysis::Array::LengthInfo::kConstant)) { return kUnknownSpan; } return ConservativeLocationSpan(array->element_type()) * std::max(lengthWords[1], 1u); } if (const auto* strct = type->AsStruct()) { Uint32 sum = 0; for (const auto* member : strct->element_types()) { sum += ConservativeLocationSpan(member); } return std::max(sum, 1u); } return kUnknownSpan; } // One BuildModule per module answers all three questions the program-scoped // demotion driver asks: which point-size capability the module declares, and // one past the highest Input/Output location slot it consumes (so the carrier // can be placed beyond every varying of every stage). struct PointSizeModuleProbe { Bool parsed = false; Bool declaresTessellationPointSize = false; Bool declaresGeometryPointSize = false; Uint32 locationSlotEnd = 0; }; PointSizeModuleProbe ProbePointSizeModule(const Vector& spirv) { PointSizeModuleProbe probe; if (spirv.empty()) { probe.parsed = true; // an absent stage constrains nothing return probe; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbePointSizeModule"), spirv.data(), spirv.size()); if (!context) return probe; probe.parsed = true; for (const spvtools::opt::Instruction& capability : context->capabilities()) { if (capability.NumInOperands() < 1) continue; const auto declared = static_cast(capability.GetSingleWordInOperand(0)); if (declared == spv::Capability::TessellationPointSize) { probe.declaresTessellationPointSize = true; } else if (declared == spv::Capability::GeometryPointSize) { probe.declaresGeometryPointSize = true; } } spv::ExecutionModel model = spv::ExecutionModel::Max; for (spvtools::opt::Instruction& entryPoint : context->module()->entry_points()) { model = static_cast(entryPoint.GetSingleWordInOperand(0)); break; } // Per-vertex interfaces are arrayed one level deeper than the locations // they consume; peel that level, but never off a per-patch output. const Bool peelInputs = model == spv::ExecutionModel::TessellationControl || model == spv::ExecutionModel::TessellationEvaluation || model == spv::ExecutionModel::Geometry; const Bool peelOutputs = model == spv::ExecutionModel::TessellationControl; std::unordered_set patchDecorated; for (spvtools::opt::Instruction& annotation : context->annotations()) { if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 2 && static_cast(annotation.GetSingleWordInOperand(1)) == spv::Decoration::Patch) { patchDecorated.insert(annotation.GetSingleWordInOperand(0)); } } auto* defUse = context->get_def_use_mgr(); auto* typeMgr = context->get_type_mgr(); for (spvtools::opt::Instruction& annotation : context->annotations()) { if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 3 && static_cast(annotation.GetSingleWordInOperand(1)) == spv::Decoration::Location) { const Uint32 location = annotation.GetSingleWordInOperand(2); Uint32 span = 1; spvtools::opt::Instruction* var = defUse->GetDef(annotation.GetSingleWordInOperand(0)); if (var != nullptr && var->opcode() == spv::Op::OpVariable) { const auto storage = static_cast(var->GetSingleWordInOperand(0)); // Two location namespaces are NOT varying slots and must not // shrink the carrier budget: vertex-stage inputs (attribute // locations) and fragment-stage outputs (draw buffers). if ((model == spv::ExecutionModel::Vertex && storage == spv::StorageClass::Input) || (model == spv::ExecutionModel::Fragment && storage == spv::StorageClass::Output)) { continue; } spvtools::opt::Instruction* pointerType = defUse->GetDef(var->type_id()); if (pointerType != nullptr && pointerType->opcode() == spv::Op::OpTypePointer) { const opt_analysis::Type* pointee = typeMgr->GetType(pointerType->GetSingleWordInOperand(1)); const Bool peel = ((storage == spv::StorageClass::Input && peelInputs) || (storage == spv::StorageClass::Output && peelOutputs)) && patchDecorated.count(var->result_id()) == 0; if (peel && pointee != nullptr && pointee->AsArray() != nullptr) { pointee = pointee->AsArray()->element_type(); } span = ConservativeLocationSpan(pointee); } } probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span); } else if (annotation.opcode() == spv::Op::OpMemberDecorate && annotation.NumInOperands() >= 4 && static_cast(annotation.GetSingleWordInOperand(2)) == spv::Decoration::Location) { const Uint32 member = annotation.GetSingleWordInOperand(1); const Uint32 location = annotation.GetSingleWordInOperand(3); Uint32 span = 1; spvtools::opt::Instruction* structType = defUse->GetDef(annotation.GetSingleWordInOperand(0)); if (structType != nullptr && structType->opcode() == spv::Op::OpTypeStruct && member < structType->NumInOperands()) { span = ConservativeLocationSpan( typeMgr->GetType(structType->GetSingleWordInOperand(member))); } probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span); } } return probe; } // Interior boundary carrier names, spelled by the PRODUCING stage so both // sides of one boundary agree textually as well as by location. The capture // stage's output uses POINT_SIZE_CAPTURE_CARRIER_NAME instead. None of these // may embed the token "gl_PointSize" - see the constant's comment. const char* PointSizeBoundaryCarrierName(const GLenum producerStage) { switch (producerStage) { case GL_VERTEX_SHADER: return "mg_PointSizeIo0"; case GL_TESS_CONTROL_SHADER: return "mg_PointSizeIo1"; case GL_TESS_EVALUATION_SHADER: return "mg_PointSizeIo2"; default: return "mg_PointSizeIo0"; } } // Past this the carrier would sit above what a minimum-spec varying budget can // address; such a program keeps its honest decline instead. constexpr Uint32 kMaxDemotedPointSizeCarrierLocation = 30; } // namespace Bool ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( Vector>& modules, const Vector& shaderTypes, const Bool demoteTessellation, const Bool demoteGeometry, const Bool captureRequestsPointSize, PointSizeDemotionOutcome& outcome, const bool validateOutput, const bool enableSpirvValidation) { outcome = {}; if (!demoteTessellation && !demoteGeometry) return true; // The pre-rasterization chain, in pipeline order, as indices into `modules`. Int stageIndex[4] = {-1, -1, -1, -1}; // VS, TCS, TES, GS for (SizeT i = 0; i < shaderTypes.size() && i < modules.size(); ++i) { switch (shaderTypes[i]) { case GL_VERTEX_SHADER: stageIndex[0] = static_cast(i); break; case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast(i); break; case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast(i); break; case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast(i); break; default: break; } } if (stageIndex[1] < 0 && stageIndex[2] < 0 && stageIndex[3] < 0) return true; // One probe per module: the capability facts arm the verdict, the location // scan places the carrier past every varying of every stage (the location is // shared program-wide, so it has to clear all of them at once). Bool anyTessellationUse = false; Bool anyGeometryUse = false; Uint32 carrierLocation = 0; for (const auto& module : modules) { const PointSizeModuleProbe probe = ProbePointSizeModule(module); if (!probe.parsed) { // Unparseable is not a verdict; the module is already broken for // other reasons and owns its own failure. return true; } anyTessellationUse |= probe.declaresTessellationPointSize; anyGeometryUse |= probe.declaresGeometryPointSize; carrierLocation = std::max(carrierLocation, probe.locationSlotEnd); } if (!((anyTessellationUse && demoteTessellation) || (anyGeometryUse && demoteGeometry))) { return true; } if (carrierLocation > kMaxDemotedPointSizeCarrierLocation) { outcome.declineDetail = std::format( "the program's varyings already reach location {}, past the carrier budget", carrierLocation); return true; } // GL 4.6 core 13.3: capture reads the last capture-capable stage - geometry, // else evaluation, else the vertex stage (whose built-in needs no demotion). const Int captureStage = stageIndex[3] >= 0 ? 3 : (stageIndex[2] >= 0 ? 2 : -1); constexpr GLenum kStageEnum[4] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER}; // Back to front, so each stage's "I now read the carrier" report can force the // producing stage's output carrier into existence - Vulkan requires every // consumed input to be produced (VUID-RuntimeSpirv-OpEntryPoint-08743), and an // ES link may reject a statically read input with no producing output. Vector> rewritten(modules.size()); Bool rewrote[4] = {false, false, false, false}; Bool forceOutput[4] = {false, false, false, false}; if (captureStage >= 0 && captureRequestsPointSize) { forceOutput[captureStage] = true; } for (Int stage = 3; stage >= 0; --stage) { const Int moduleIndex = stageIndex[stage]; if (moduleIndex < 0) continue; Int producer = stage - 1; while (producer >= 0 && stageIndex[producer] < 0) --producer; DemotePointSizeOptions options; options.location = carrierLocation; options.inputCarrierName = PointSizeBoundaryCarrierName( producer >= 0 ? kStageEnum[producer] // A separable program whose first present stage already // consumes the carrier: the producer lives in another // program. Name by the conventional producer of this // stage's boundary; matching across programs is by // location and is documented residue either way. : kStageEnum[stage > 0 ? stage - 1 : 0]); options.outputCarrierName = stage == captureStage ? String(POINT_SIZE_CAPTURE_CARRIER_NAME) : String(PointSizeBoundaryCarrierName(kStageEnum[stage])); options.forceOutputCarrier = forceOutput[stage]; // A vertex stage with nothing downstream consuming the carrier needs no // mirror and stays byte-identical without an optimizer round trip. if (stage == 0 && !options.forceOutputCarrier) continue; DemotePointSizeReport report; spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass( DemotePointSizePass::CreateDemotePointSizePass(options, &report)); if (!RunOptimizerChecked("DemoteTessellationGeometryPointSizeForProgram", optimizer, modules[moduleIndex], rewritten[moduleIndex], validateOutput, enableSpirvValidation)) { return false; // modules untouched: nothing was committed } if (report.declined) { outcome.declineDetail = Move(report.declineReason); return true; // byte-identical decline; the existing refusals stay armed } // AN EVALUATION STAGE WITH NO CONTROL STAGE THAT NOW READS A LOCATED // INPUT. GL lets the evaluation stage sit straight on the vertex stage, // and both backends stand a SYNTHESIZED pass-through control stage in // between - one that forwards gl_Position and nothing else. Their guard // for that is literally "does this module read a located input" // (ModuleReadsLocatedInput / ReflectPassthroughTessControlNeed), so the // carrier this pass just created would turn the very program the demotion // exists to rescue into a declined one, reported against a varying name // the application never wrote. Declining here keeps the modules // byte-identical and leaves the honest built-in refusal in charge; only // teaching the synthesized stage to forward the carrier could do better. if (stage == 2 && stageIndex[1] < 0 && report.createdInputCarrier) { outcome.declineDetail = "an evaluation stage reads gl_in point size with no control stage to " "carry it; the synthesized pass-through cannot forward the carrier"; return true; } rewrote[stage] = true; if (report.createdInputCarrier && producer >= 0) { forceOutput[producer] = true; } } // Atomic commit: every stage rewritten together or none at all. for (Int stage = 0; stage < 4; ++stage) { if (!rewrote[stage]) continue; modules[stageIndex[stage]] = Move(rewritten[stageIndex[stage]]); } outcome.demoted = true; return true; } Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector& spirv) { if (spirv.empty()) { // Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced // no SPIR-V is not a verdict about 64-bit floats, and parsing it would push a // spurious diagnostic through the message consumer. return false; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64"), spirv.data(), spirv.size()); if (!context) { return false; } for (const spvtools::opt::Instruction& type : context->types_values()) { if (type.opcode() == spv::Op::OpTypeFloat && type.NumInOperands() >= 1 && type.GetSingleWordInOperand(0) == 64) { return true; } } return false; } namespace { // The leaf-width test behind ModuleDeclaresFloat64VertexInput, and it is a LEAF // test rather than a shape test on purpose: a `dmat4` input is an OpTypeMatrix of // OpTypeVector of OpTypeFloat 64, and it is as unfetchable as a bare `double`. Bool TypeHoldsFloat64(const spvtools::opt::analysis::Type* type) { if (type == nullptr) return false; if (const auto* scalar = type->AsFloat()) return scalar->width() == 64; if (const auto* vector = type->AsVector()) return TypeHoldsFloat64(vector->element_type()); if (const auto* matrix = type->AsMatrix()) return TypeHoldsFloat64(matrix->element_type()); if (const auto* array = type->AsArray()) return TypeHoldsFloat64(array->element_type()); return false; } } // namespace Bool ShaderCompiler::ModuleDeclaresFloat64VertexInput(const Vector& spirv) { if (spirv.empty()) { return false; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64VertexInput"), spirv.data(), spirv.size()); if (!context) { return false; } // Vertex only. Every other stage's inputs come from another stage's outputs, which // MobileGL never re-formats, so a 64-bit varying between two stages is the driver's // business and not this question's. auto entryPoints = context->module()->entry_points(); if (entryPoints.begin() == entryPoints.end()) return false; const spvtools::opt::Instruction& entryPoint = *entryPoints.begin(); if (static_cast(entryPoint.GetSingleWordInOperand(0)) != spv::ExecutionModel::Vertex) { return false; } auto* typeManager = context->get_type_mgr(); auto* defUseManager = context->get_def_use_mgr(); for (const spvtools::opt::Instruction& variable : context->module()->types_values()) { if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) continue; if (static_cast(variable.GetSingleWordInOperand(0)) != spv::StorageClass::Input) { continue; } const spvtools::opt::Instruction* pointerType = defUseManager->GetDef(variable.type_id()); if (pointerType == nullptr || pointerType->NumInOperands() < 2) continue; if (TypeHoldsFloat64(typeManager->GetType(pointerType->GetSingleWordInOperand(1)))) { return true; } } return false; } Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector& spirv) { if (spirv.empty()) { return false; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleReadsLocatedInput"), spirv.data(), spirv.size()); if (!context) { return false; } // A LOCATION is exactly the property that separates a user-defined varying (or a // per-patch input) from a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn, // gl_PrimitiveID and the tessellation levels carry none, and every one of them is // either forwarded by the pass-through or generated by the tessellator itself. // // Decided on the OpVariable's own Location decoration rather than on any // built-in classification, for the reason DirectVulkan's // ReflectPassthroughTessControlNeed records at length: gl_in is an ARRAY OF // INTERFACE BLOCKS, and a member walk of one reads back as BuiltIn::Position for // every member, so classifying by built-in would accept anything. for (auto& variable : context->module()->types_values()) { if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) { continue; } if (static_cast(variable.GetSingleWordInOperand(0)) != spv::StorageClass::Input) { continue; } Bool located = false; context->get_decoration_mgr()->ForEachDecoration( variable.result_id(), static_cast(spv::Decoration::Location), [&located](const spvtools::opt::Instruction&) { located = true; }); if (located) return true; } return false; } bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass()); return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector& inputBinary, Vector& outputBinary, const bool validateOutput, const bool enableSpirvValidation, const bool nativeFloat64) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); // ADCE refuses to treat a Private global as deletable while the entry point // still contains any OpFunctionCall (IsLocalVar -> IsEntryPointWithNoCalls), so // a dead vertex input feeding a never-read Private shim used to survive the // whole chain (the Chocapic13 shadow.vsh mc_midTexCoord/iris_MidTex case). // Rewriting entry-point-owned Private variables to Function storage first // satisfies ADCE without inlining: over 521 real Iris modules the rewrite // captured 17 of the 21 extra dead interface variables exhaustive inlining // would, while shrinking the corpus 8% - inlining grew it 20% with a 5.3x // worst-case module and no additional GPU-side benefit. optimizer.RegisterPass(PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass()); // Keep the one-arg overload: remove_outputs must stay false, forever. Output // variables on the entry-point interface are ADCE's only unconditional live // roots; XFB capture resolves varyings by OpName after this chain, and the // VS-out/FS-in interface contract on both backends depends on declared outputs // surviving even when never stored. optimizer.RegisterPass(CreateAggressiveDCEPass(false)); // Complementary to ADCE, not redundant: ADCE can never delete or delist an // Output (see above), so never-written outputs are trimmed from the // OpEntryPoint operand list here. optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass()); // The two module-legality repairs, so the chain's output - the bytes every // consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to // live only in the backends; a validating lane would flag every rectangle // module long before the backend got the chance to fix it, and the backend // calls remain as no-ops on the now rect-free modules. optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); optimizer.RegisterPass(StripUniformLocationsPass::CreateStripUniformLocationsPass()); optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass()); optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass()); optimizer.RegisterPass( RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass()); optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass()); optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass()); // The fp64 tail, and the ONE part of this chain that is not the same on every // backend. Both passes are skipped when the backend can consume Float64 itself // (`nativeFloat64`, i.e. VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan): // there is nothing to emulate then, and narrowing would only throw away precision // the driver was willing to give. That is DirectVulkan-on-lavapipe today and // nothing else - Adreno and Mali both report shaderFloat64 == VK_FALSE, and // DirectGLES can never qualify because GLSL ES has no fp64 type for SPIRV-Cross to // emit at all, so on every real mobile device this branch is not taken and the two // passes run exactly as they always have. // // Demoting here - in the one chain every module goes through, at link - is what // makes `double` compile at all where the hardware has none, and makes it behave // the SAME across both backends of such a device, which matters because the GL // frontend's uniform storage is per PROGRAM rather than per call: the glUniform*d // shadow narrows to float to match this. Runs last so no earlier pass ever has to // reason about a width it will not see in the output; in particular it runs before // the backends' PackDoubleVertexInputsPass, whose OpBitcast this one would // otherwise decline on. Costs one types_values() walk on the overwhelming majority // of modules, which declare no 64-bit float at all. // ...but demoting a double that lives in a SHADER STORAGE BLOCK also repacks that // block, and the bytes an application put in the buffer do not move with it. This // runs first and takes those blocks out of the demotion's hands: each becomes a // flat `uint` array whose index arithmetic carries the std140/std430 offsets // glslang computed WITH the doubles in place, so the layout survives byte for byte // and only the VALUES narrow. Gated on a block actually holding a 64-bit float, so // every other module pays one types_values() walk and nothing else, and it declines // (leaving the block for the demotion to handle the old way) on any shape it cannot // re-address exactly. See FlattenFloat64StorageBlockPass.h. It is skipped with the // demotion rather than kept: its whole purpose is to preserve the byte layout ACROSS // a narrowing that is no longer happening, and flattening a block a native driver // would have laid out correctly by itself only costs the shader its index // arithmetic. if (!nativeFloat64) { optimizer.RegisterPass( FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass()); optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass()); } return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary, outputBinary, validateOutput, enableSpirvValidation); } bool ShaderCompiler::LowerDrawParametersForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass()); return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::LowerViewportIndexForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(LowerViewportIndexPass::CreateLowerViewportIndexPass()); return RunOptimizerChecked("LowerViewportIndexForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DeferGeometryEmitsForEssl(const Vector& inputBinary, Vector& outputBinary, bool& outDeferred, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); Bool deferred = false; optimizer.RegisterPass(DeferGeometryEmitsPass::CreateDeferGeometryEmitsPass(&deferred)); const bool ok = RunOptimizerChecked("DeferGeometryEmitsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); outDeferred = ok && deferred; return ok; } bool ShaderCompiler::DeclaresViewportIndexBuiltin(const Vector& binary) { return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary); } bool ShaderCompiler::ClampMultisampleFetchesForEssl(const Vector& inputBinary, Vector& outputBinary, const Int32 maxColorSamples, const Int32 maxIntegerSamples, const Int32 maxDepthSamples, const Int32 advertisedMaxSamples, const bool enableSpirvValidation, const bool deferDirectUnsignedFetches) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass( maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples, deferDirectUnsignedFetches)); return RunOptimizerChecked("ClampMultisampleFetchesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DeclaresMultisampledImage(const Vector& binary) { return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary); } ShaderCompiler::SpirvGateFeatures ShaderCompiler::ProbeSpirvGateFeatures( const Vector& binary) { SpirvGateFeatures features; if (binary.empty()) { return features; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}, binary.data(), binary.size()); if (!context) { // Unparseable here means unusable downstream too; let the ordinary transpile // path produce the error rather than inventing a verdict from it. return features; } features.WritesViewportIndexOutput = LowerViewportIndexPass::DeclaresViewportIndexBuiltin(context.get()); features.DeclaresMultisampledImage = ClampMultisampleFetchPass::DeclaresMultisampledImage(context.get()); return features; } bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass()); return RunOptimizerChecked("SplitArrayVertexInputsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::BakeImageFormatsForEssl(const Vector& inputBinary, const UnorderedMap& glFormatByName, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; if (glFormatByName.empty()) return false; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(BakeImageFormatsPass::CreateBakeImageFormatsPass(glFormatByName)); return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DeclaresFormatlessStorageImage(const Vector& binary) { return BakeImageFormatsPass::DeclaresFormatlessStorageImage(binary); } bool ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat) { return BakeImageFormatsPass::IsCoreEsslImageFormat( BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat)); } String ShaderCompiler::EsslImageFormatSpelling(Uint glInternalFormat) { return BakeImageFormatsPass::EsslSpellingOfGLInternalFormat(glInternalFormat); } bool ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat) { return BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat( BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat)); } bool ShaderCompiler::WidenImageFormatsForEssl(const Vector& inputBinary, Vector& outputBinary, const bool onlyFormatsSpirvCrossRefusesToPrint, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass( WidenImageFormatsPass::CreateWidenImageFormatsPass(onlyFormatsSpirvCrossRefusesToPrint)); // Two image types that differed only in a format the widening collapses - // `layout(rg32f)` and `layout(rgba32f)` in one module - are one type afterwards, // and duplicate non-aggregate type declarations are invalid SPIR-V. This joins // them, and cascades to the pointer and array types that named them; the pass // itself deliberately does not carry a join of its own. optimizer.RegisterPass(CreateRemoveDuplicatesPass()); return RunOptimizerChecked("WidenImageFormatsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector& binary, const bool onlyFormatsSpirvCrossRefusesToPrint) { return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary, onlyFormatsSpirvCrossRefusesToPrint); } Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) { return WidenImageFormatsPass::WidenedCoreEsslImageFormat(glInternalFormat); } Uint ShaderCompiler::ImageFormatChannelCount(Uint glInternalFormat) { return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat); } bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4], bool& outSignedNormalized) { return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax, outSignedNormalized); } Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) { return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat); } bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, const std::set& blockNames, std::set& flattenedBlockNames, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; if (blockNames.empty()) return false; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(FlattenXfbInterfaceBlocksPass::CreateFlattenXfbInterfaceBlocksPass( blockNames, &flattenedBlockNames)); return RunOptimizerChecked("FlattenXfbInterfaceBlocksForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock( const String& captureName, const std::set& flattenedBlockNames, String& outName) { return FlattenXfbInterfaceBlocksPass::RewriteCaptureName(captureName, flattenedBlockNames, outName); } void ShaderCompiler::ProbeIoBlockNamesForEssl(const Vector& binary, std::set& collidingBlockNames, std::set& declaredNames) { if (binary.empty()) { // Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced // no SPIR-V has no block names to report, and parsing it would push a // spurious diagnostic through the message consumer. return; } std::unique_ptr context = spvtools::BuildModule( SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbeIoBlockNamesForEssl"), binary.data(), binary.size()); if (!context) { // Unparseable here means unusable downstream too; let the ordinary transpile // path produce the error rather than inventing a rename plan from it. return; } UniquifyIoBlockNamesPass::ProbeIoBlockNames(context.get(), collidingBlockNames, declaredNames); } bool ShaderCompiler::UniquifyIoBlockNamesForEssl(const Vector& inputBinary, const std::map& inputBlockRenames, const std::map& outputBlockRenames, std::set& renamedBlockNames, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; if (inputBlockRenames.empty() && outputBlockRenames.empty()) return false; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(UniquifyIoBlockNamesPass::CreateUniquifyIoBlockNamesPass( inputBlockRenames, outputBlockRenames, &renamedBlockNames)); return RunOptimizerChecked("UniquifyIoBlockNamesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::StripIoBlockLocationsForEssl(const Vector& inputBinary, const bool stripInputBlocks, const bool stripOutputBlocks, bool& strippedAny, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; strippedAny = false; if (!stripInputBlocks && !stripOutputBlocks) return false; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(StripIoBlockLocationsPass::CreateStripIoBlockLocationsPass( stripInputBlocks, stripOutputBlocks, &strippedAny)); // NOT VALIDATED, and that is the point of the pass rather than an oversight. // Vulkan SPIR-V requires a Location on every user-defined Input/Output variable // ([VUID-StandaloneSpirv-Location-04915]), so a module whose interface blocks // have deliberately lost theirs fails spirv-val by construction. It never // reaches a driver as SPIR-V: the caller runs this last in the DirectGLES chain // and hands the result straight to SPIRV-Cross, which needs no location to // print a block. Validating here would latch a failure on every affected // program and teach the counter to cry wolf. return RunOptimizerChecked("StripIoBlockLocationsForEssl", optimizer, inputBinary, outputBinary, false, enableSpirvValidation); } bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass()); return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass( StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass()); return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass()); return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass()); return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; // Detection gates everything: a module with no dynamically indexed fragment // output - every shader but a handful - pays one BuildModule and is handed // back byte for byte, so the folding chain can never perturb a shader that // did not need it. if (!LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(inputBinary)) { outputBinary = inputBinary; return true; } // Stock passes do the real work. The only bespoke member is the loop-control // hint the stock unroller demands (see the pass header); with it set, an index // derived from a loop counter - the shape of the Minecraft 26.3 OIT // coefficient shader and of most real ones - folds to a literal here, and the // fallback below never runs. Optimizer folder(SPV_ENV_VULKAN_1_1); // First, because both the unroller and the marking pass below read the // induction variable as an OpPhi, and glslang emits it as loads and stores of // a Function variable. folder.RegisterPass(CreateLocalMultiStoreElimPass()); folder.RegisterPass(LegalizeFragmentOutputIndexPass::CreateMarkLoopsForUnrollPass()); folder.RegisterPass(CreateLoopUnrollPass(true)); // Fold the unrolled induction values into the access chains, then clear out // what constant conditions leave behind. folder.RegisterPass(CreateCCPPass()); folder.RegisterPass(CreateSimplificationPass()); folder.RegisterPass(CreateDeadBranchElimPass()); folder.RegisterPass(CreateBlockMergePass()); Vector folded; if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.fold", folder, inputBinary, folded, true, enableSpirvValidation) || folded.empty()) { // Fail open onto the fallback rather than onto the illegal module. folded = inputBinary; } if (!LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(folded)) { outputBinary = folded; return true; } // Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it. Optimizer lowerer(SPV_ENV_VULKAN_1_1); lowerer.RegisterPass(LegalizeFragmentOutputIndexPass::CreateLowerToConstantSwitchPass()); // The chains the lowering replaced are dead now; remove_outputs must stay // false here for the same reason it does in SanitizeAndOptimizeBinary. lowerer.RegisterPass(CreateAggressiveDCEPass(false)); if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.lower", lowerer, folded, outputBinary, true, enableSpirvValidation) || outputBinary.empty()) { outputBinary = folded; return true; } if (LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(outputBinary)) { // MGLOG_W, latched: this runs per shader compile, and shader packs compile // lazily mid-session, so an unlatched line here is unbounded runtime noise. // (Parked at MGLOG_I until the Log.h ordering fix made W live at INFO.) MGLOG_W_ONCE("[spirv] LegalizeFragmentOutputIndexingForEssl: a fragment output is still " "indexed dynamically; a strict ES driver will reject this shader"); } return true; } bool ShaderCompiler::LegalizeResourceArrayIndexingForEssl( const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; // Detection gates everything: a module that declares no array of storage blocks // and no array of images, or indexes one only with constants - every shader but // a handful - pays one BuildModule and is handed back byte for byte, so the // folding chain can never perturb a shader that did not need it. if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing( inputBinary)) { outputBinary = inputBinary; return true; } // Stock passes do the real work, exactly as in the fragment-output // legalization. The only bespoke member of the chain is the loop-control hint // the stock unroller demands (see the pass header); with it set, the // `for (i = 0; i < 4; ++i) arr[i]...` shape folds to literals here and the // fallback below never runs. Optimizer folder(SPV_ENV_VULKAN_1_1); // First, because both the unroller and the marking pass below read the // induction variable as an OpPhi, and glslang emits it as loads and stores of // a Function variable. folder.RegisterPass(CreateLocalMultiStoreElimPass()); folder.RegisterPass(LegalizeResourceArrayIndexPass::CreateMarkLoopsForUnrollPass()); folder.RegisterPass(CreateLoopUnrollPass(true)); // Fold the unrolled induction values into the access chains, then clear out // what constant conditions leave behind. folder.RegisterPass(CreateCCPPass()); folder.RegisterPass(CreateSimplificationPass()); folder.RegisterPass(CreateDeadBranchElimPass()); folder.RegisterPass(CreateBlockMergePass()); Vector folded; if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.fold", folder, inputBinary, folded, true, enableSpirvValidation) || folded.empty()) { // Fail open onto the fallback rather than onto the illegal module. folded = inputBinary; } if (!LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing( folded)) { outputBinary = folded; return true; } // Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it. Optimizer lowerer(SPV_ENV_VULKAN_1_1); lowerer.RegisterPass( LegalizeResourceArrayIndexPass::CreateLowerToConstantSwitchPass()); // The chains the lowering replaced are dead now; remove_outputs must stay // false here for the same reason it does in SanitizeAndOptimizeBinary. lowerer.RegisterPass(CreateAggressiveDCEPass(false)); if (!RunOptimizerChecked("LegalizeResourceArrayIndexingForEssl.lower", lowerer, folded, outputBinary, true, enableSpirvValidation) || outputBinary.empty()) { outputBinary = folded; return true; } if (LegalizeResourceArrayIndexPass::BinaryHasDynamicResourceArrayIndexing( outputBinary)) { // MGLOG_W, latched, for the same reason the fragment-output one is: this // runs per shader compile and shader packs compile lazily mid-session. MGLOG_W_ONCE("[spirv] LegalizeResourceArrayIndexingForEssl: an array of storage " "blocks or of images is still indexed dynamically; a strict ES " "driver will reject this shader"); } return true; } bool ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl( const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; // Detection gates everything: a module with no atomic counter, or one whose // counters sit at their natural std430 offsets - which is every shader that omits // the offset qualifier - pays one BuildModule and is handed back byte for byte. if (!FlattenAtomicCounterBlockPass::BinaryHasOffsetAtomicCounterBlock(inputBinary)) { outputBinary = inputBinary; return true; } Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(FlattenAtomicCounterBlockPass::CreateFlattenAtomicCounterBlockPass()); return RunOptimizerChecked("FlattenAtomicCounterBlockOffsetsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::LowerRectImages(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); // Rect and 2D images of the same format become the same type after lowering. optimizer.RegisterPass(DeduplicateImageTypesPass::Create()); return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::Lower1DArrayImagesForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; // Declined rather than half-translated: after the rewrite the image is a 2D // (array) one, so a size query on it yields a component more than the shader // consumes. Handing back a differently-shaped size silently is worse than leaving // the module alone and letting the driver say what it does not like - and unlike // the access path there is no correct answer to substitute, because the ES texture // genuinely has a height the GL one does not. // // MGLOG_W, latched: per shader compile, and shader packs compile lazily // mid-session. (Parked at MGLOG_I until the Log.h ordering fix made W live.) const auto traits = Lower1DArrayImagesPass::InspectBinary(inputBinary); // The overwhelmingly common answer, and the reason the inspection exists: no // 1D storage image this pass owns, so the module is handed back byte for byte // without an Optimizer ever being built. Every ESSL shader in the process passes // through here, so the cost of the case with nothing to do is the cost of this // pass. if (!traits.declaresImage) { outputBinary = inputBinary; return true; } if (traits.queriesImageSize) { MGLOG_W_ONCE("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D " "storage image, which cannot be answered in the 2D shape ES stores it in; " "leaving the module alone, and a strict ES driver will reject it"); outputBinary = inputBinary; return true; } Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass()); // Mandatory, not tidying. Rewriting a 1D(-array) image type to the 2D(-array) one // makes it structurally IDENTICAL to any real 2D(-array) image of the same sampled // type and format that the module already declared - and SPIR-V forbids duplicate // non-aggregate type declarations, so the result fails validation. That collision // is not exotic: it is the shape of this whole change's headline case, where one // compute shader declares uimage1DArray and uimage2DArray side by side, both // r32ui. The same applies one level up, to the OpTypePointer instructions that // named the two types, and to the Image1D capability the rewrite turns into a // second Shader. Deduplicating afterwards collapses all three at once. optimizer.RegisterPass(CreateRemoveDuplicatesPass()); return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::Lower1DSampledImagesForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; // The overwhelmingly common answer, and the reason the probe exists: no 1D sampler // is reached by an offset or a gradient, so the module is handed back byte for // byte without an Optimizer ever being built. Every ESSL shader in the process // passes through here, so the cost of the case with nothing to do is the cost of // this pass. Note the probe is deliberately NARROWER than "declares a 1D sampler": // SPIRV-Cross emits the plain sample and fetch forms correctly, and taking those // over would be a regression looking for somewhere to happen. if (!Lower1DSampledImagesPass::BinaryHasOffsetOrGrad1DSampledImage(inputBinary)) { outputBinary = inputBinary; return true; } Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(Lower1DSampledImagesPass::CreateLower1DSampledImagesPass()); // Mandatory, not tidying - the same collision Lower1DArrayImagesForEssl documents // one screen up. Rewriting a 1D sampled image type to the 2D one makes it // structurally IDENTICAL to any real 2D sampled image of the same sampled type the // module already declared, and SPIR-V forbids duplicate non-aggregate type // declarations. That is not exotic here: it is the exact shape of the headline // case, whose compute shader declares sampler1D and sampler2D side by side. The // same applies to the OpTypeSampledImage and OpTypePointer instructions above // them, and to the Sampled1D capability the rewrite turns into a second Shader. optimizer.RegisterPass(CreateRemoveDuplicatesPass()); return RunOptimizerChecked("Lower1DSampledImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass()); return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::ZeroBaseVertexForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(ZeroBaseVertexPass::CreateZeroBaseVertexPass()); return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::LowerPixelCenterIntegerForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(LowerPixelCenterIntegerPass::CreateLowerPixelCenterIntegerPass()); return RunOptimizerChecked("LowerPixelCenterIntegerForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::ZeroLayerOutputForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(ZeroLayerOutputPass::CreateZeroLayerOutputPass()); return RunOptimizerChecked("ZeroLayerOutputForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::FullCoverageSampleMaskForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(FullCoverageSampleMaskPass::CreateFullCoverageSampleMaskPass()); return RunOptimizerChecked("FullCoverageSampleMaskForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass()); return RunOptimizerChecked("DeriveNumSubgroupsForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector& inputBinary, Vector& outputBinary, const Uint32 maxWorkgroupScratchBytes, const bool enableSpirvValidation, const bool atomicScratchReads) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes, atomicScratchReads)); return RunOptimizerChecked("EmulateSubgroupsForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan( const Vector& inputBinary, Vector& outputBinary, const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass( FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass( nativeSubgroupSize, maxWorkgroupScratchBytes)); return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::FixIterationRPBarrierForVulkan( const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass()); return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass()); return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { constexpr SizeT kSpirvHeaderWordCount = 5; outputBinary.clear(); if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) { return false; } Vector floatTypeIds; Vector resultTypeById(inputBinary[3], 0); Vector pointerPointeeTypeById(inputBinary[3], 0); Bool hasReadWithoutFormatCapability = false; Bool hasWriteWithoutFormatCapability = false; SizeT capabilityInsertOffset = kSpirvHeaderWordCount; for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { const Uint32 instructionWord = inputBinary[offset]; const Uint32 wordCount = instructionWord >> 16u; const auto opcode = static_cast(instructionWord & 0xffffu); if (wordCount == 0 || offset + wordCount > inputBinary.size()) { return false; } if (opcode == spv::Op::OpCapability && wordCount >= 2) { capabilityInsertOffset = offset + wordCount; const auto capability = static_cast(inputBinary[offset + 1]); hasReadWithoutFormatCapability |= capability == spv::Capability::StorageImageReadWithoutFormat; hasWriteWithoutFormatCapability |= capability == spv::Capability::StorageImageWriteWithoutFormat; } else if (opcode == spv::Op::OpTypeFloat && wordCount >= 3) { floatTypeIds.push_back(inputBinary[offset + 1]); } else if (opcode == spv::Op::OpTypePointer && wordCount >= 4) { const Uint32 pointerTypeId = inputBinary[offset + 1]; if (pointerTypeId >= pointerPointeeTypeById.size()) { return false; } pointerPointeeTypeById[pointerTypeId] = inputBinary[offset + 3]; } bool hasResult = false; bool hasResultType = false; spv::HasResultAndType(opcode, &hasResult, &hasResultType); if (hasResult && hasResultType && wordCount >= 3) { const Uint32 resultTypeId = inputBinary[offset + 1]; const Uint32 resultId = inputBinary[offset + 2]; if (resultId >= resultTypeById.size()) { return false; } resultTypeById[resultId] = resultTypeId; } offset += wordCount; } // OpImageTexelPointer is the bridge to image atomic instructions. Vulkan requires // those image types to retain an atomic-compatible declared format, so exclude only // the exact image types used by an atomic path rather than disabling formatless // access for unrelated float images in the same module. Vector atomicImageTypeIds; for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { const Uint32 instructionWord = inputBinary[offset]; const Uint32 wordCount = instructionWord >> 16u; const auto opcode = static_cast(instructionWord & 0xffffu); if (opcode == spv::Op::OpImageTexelPointer && wordCount >= 6) { const Uint32 imageId = inputBinary[offset + 3]; if (imageId >= resultTypeById.size()) { return false; } Uint32 imageTypeId = resultTypeById[imageId]; if (imageTypeId < pointerPointeeTypeById.size() && pointerPointeeTypeById[imageTypeId] != 0) { imageTypeId = pointerPointeeTypeById[imageTypeId]; } if (imageTypeId != 0 && std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) == atomicImageTypeIds.end()) { atomicImageTypeIds.push_back(imageTypeId); } } offset += wordCount; } outputBinary = inputBinary; Bool hasFloatStorageImage = false; for (SizeT offset = kSpirvHeaderWordCount; offset < outputBinary.size();) { const Uint32 instructionWord = outputBinary[offset]; const Uint32 wordCount = instructionWord >> 16u; const auto opcode = static_cast(instructionWord & 0xffffu); // OpTypeImage operands are: result id, sampled type, dim, depth, arrayed, // multisampled, sampled, image format, and an optional access qualifier. if (opcode == spv::Op::OpTypeImage && wordCount >= 9) { const Uint32 imageTypeId = outputBinary[offset + 1]; const Uint32 sampledTypeId = outputBinary[offset + 2]; const Uint32 sampled = outputBinary[offset + 7]; const Bool hasFloatSampledType = std::find(floatTypeIds.begin(), floatTypeIds.end(), sampledTypeId) != floatTypeIds.end(); const Bool usedByAtomic = std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) != atomicImageTypeIds.end(); if (sampled == 2 && hasFloatSampledType && !usedByAtomic) { outputBinary[offset + 8] = static_cast(spv::ImageFormat::Unknown); hasFloatStorageImage = true; } } offset += wordCount; } if (!hasFloatStorageImage) { return true; } Vector addedCapabilities; const Uint32 capabilityInstruction = (2u << 16u) | static_cast(spv::Op::OpCapability); if (!hasReadWithoutFormatCapability) { addedCapabilities.push_back(capabilityInstruction); addedCapabilities.push_back( static_cast(spv::Capability::StorageImageReadWithoutFormat)); } if (!hasWriteWithoutFormatCapability) { addedCapabilities.push_back(capabilityInstruction); addedCapabilities.push_back( static_cast(spv::Capability::StorageImageWriteWithoutFormat)); } outputBinary.insert(outputBinary.begin() + static_cast(capabilityInsertOffset), addedCapabilities.begin(), addedCapabilities.end()); // Erasing the format can make distinct image types identical. Use the same // image-only cleanup as rectangle lowering; named blocks retain their identity. spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(DeduplicateImageTypesPass::Create()); Vector rewrittenBinary = std::move(outputBinary); return RunOptimizerChecked("UseUnformattedFloatStorageImagesForVulkan", optimizer, rewrittenBinary, outputBinary, true, enableSpirvValidation); } namespace { // The execution model an application-supplied module's entry point must carry for // the shader object it was handed to. glShaderBinary attaches a module to a shader // of a fixed type, and ARB_gl_spirv requires the specialized entry point to match. SpvExecutionModel ExecutionModelForShaderType(GLenum shaderType) { switch (shaderType) { case GL_VERTEX_SHADER: return SpvExecutionModelVertex; case GL_TESS_CONTROL_SHADER: return SpvExecutionModelTessellationControl; case GL_TESS_EVALUATION_SHADER: return SpvExecutionModelTessellationEvaluation; case GL_GEOMETRY_SHADER: return SpvExecutionModelGeometry; case GL_COMPUTE_SHADER: return SpvExecutionModelGLCompute; case GL_FRAGMENT_SHADER: default: return SpvExecutionModelFragment; } } // The decorated capture layout, as the equivalent glTransformFeedbackVaryings // request. GL 4.6 core 11.1.2.1 / ARB_transform_feedback3 give the name list two // pseudo-varyings that are exactly what a decoration layout needs: gl_NextBuffer // moves to the next capture buffer, and gl_SkipComponentsN (N in 1..4) advances the // cursor without capturing. Together they can express any offset/stride layout // whose offsets are component-aligned, which SPIR-V's are (Offset is in bytes and // xfb offsets are four-byte aligned by rule). Vector BuildXfbVaryingRequest(const Vector& captures) { Vector names; if (captures.empty()) return names; auto emitSkip = [&names](Uint32 components) { while (components > 0) { const Uint32 step = std::min(components, 4); names.push_back("gl_SkipComponents" + std::to_string(step)); components -= step; } }; Uint32 currentBuffer = captures.front().buffer; Uint32 cursorComponents = 0; Uint32 currentStride = 0; // Buffers below the first captured one still have to be stepped over, so the // Nth gl_NextBuffer really does land on buffer N. for (Uint32 buffer = 0; buffer < currentBuffer; ++buffer) { names.push_back("gl_NextBuffer"); } for (const SpirvXfbCapture& capture : captures) { if (capture.buffer != currentBuffer) { // Pad the buffer being left out to its declared stride, so the record // size the module asked for survives. if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents); for (Uint32 buffer = currentBuffer; buffer < capture.buffer; ++buffer) { names.push_back("gl_NextBuffer"); } currentBuffer = capture.buffer; cursorComponents = 0; currentStride = 0; } const Uint32 offsetComponents = capture.offset / 4; if (offsetComponents > cursorComponents) emitSkip(offsetComponents - cursorComponents); names.push_back(capture.name); cursorComponents = offsetComponents + capture.componentCount; currentStride = std::max(currentStride, capture.stride); } if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents); return names; } } // namespace Result ShaderCompiler::ValidateSpirvModule(const Vector& spirv) { ResultInfo r; r.errc = -6; if (spirv.size() < 5) { r.log = "Error: [ARB_gl_spirv] the module is too short to be SPIR-V."; return std::unexpected(r); } // 0x07230203 is SPIR-V's magic number. A module in the other byte order is a // legal SPIR-V file but NOT one glShaderBinary accepts: ARB_gl_spirv fixes the // word order to the host's. if (spirv[0] != 0x07230203u) { r.log = "Error: [ARB_gl_spirv] the module does not begin with the SPIR-V magic number."; return std::unexpected(r); } PrepareSpirvValidation(); spvtools::SpirvTools tools(SPV_ENV_OPENGL_4_5); String diagnostics; tools.SetMessageConsumer([&diagnostics](spv_message_level_t, const char*, const spv_position_t&, const char* message) { if (!diagnostics.empty()) diagnostics += "\n"; diagnostics += message ? message : ""; }); if (!tools.Validate(spirv.data(), spirv.size())) { r.log = "Error: [ARB_gl_spirv] the module failed SPIR-V validation:\n" + diagnostics; return std::unexpected(r); } return {}; } Result ShaderCompiler::SpecializeAndDecompileSpirvModule( const Vector& spirv, GLenum shaderType, const String& entryPoint, const Vector& constantIds, const Vector& constantValues, SpecializationFailure& outFailure) { outFailure = SpecializationFailure::None; SpvcSession session(spirv, SessionUsageBit::Transpile); if (!session.IsTranspileReady()) { // SPIRV-Cross could not parse the module. glShaderBinary's spirv-val pass is a // validity check, not a parseability one, so this is reachable with a module // that validates - hence a diagnosis rather than the null dereference the // unchecked constructor used to walk into. outFailure = SpecializationFailure::ModuleRejected; ResultInfo r; r.errc = -11; r.log = "Error: [ARB_gl_spirv] the module could not be parsed:\n" + String(session.GetLastErrorString()); return std::unexpected(r); } Uint32 unknownConstantId = 0; if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) { outFailure = SpecializationFailure::UnknownConstantId; ResultInfo r; r.errc = -7; r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) + " is not a specialization constant of this module."; return std::unexpected(r); } // No `if (!entryPoint.empty())` guard any more. ARB_gl_spirv makes pEntryPoint the // name of the entry point to specialize, and no module carries one named ""; the // guard turned an empty name into "whichever entry point happens to be default", // which is neither what the application asked for nor an error it was told about. if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) != SPVC_SUCCESS) { outFailure = SpecializationFailure::UnknownEntryPoint; ResultInfo r; r.errc = -8; r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint + "' for this shader stage:\n" + String(session.GetLastErrorString()); return std::unexpected(r); } // Read the declared capture layout, then REMOVE the decorations that describe it. // Both halves matter: without the read a SPIR-V program captures nothing, and // without the strip the decorations round-trip through the emitted GLSL back into // the regenerated SPIR-V, where DirectGLES's ESSL hop refuses them outright and // loses the stage. See SpvcSession::StripTransformFeedbackDecorations. SpecializedModule specialized; specialized.xfbVaryings = BuildXfbVaryingRequest(session.ReflectTransformFeedbackCaptures()); session.StripTransformFeedbackDecorations(); spvc_compiler_options options; if (session.CreateOptions(&options) != SPVC_SUCCESS) { outFailure = SpecializationFailure::ModuleRejected; ResultInfo r; r.errc = -9; r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module."; return std::unexpected(r); } // DESKTOP 4.60, not the ESSL 3.20 DecompileShader emits: this source goes back in // at the FRONT of the pipeline, to be parsed by glslang exactly like an // application's own GLSL, and every one of MobileGL's source-level passes is // written against the desktop dialect. The ESSL hop happens later and unchanged, // out of the SPIR-V this re-parse produces. spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 460); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE); // Vulkan semantics OFF is what makes this a GL source: descriptor sets collapse // onto GL binding points, push constants become a uniform block, and - the point // of the specialization pass above - every specialization constant is folded in // as a literal instead of re-emitted as layout(constant_id = N). spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, SPVC_TRUE); session.SetOptions(options); const char* emitted = nullptr; session.Compile(&emitted); if (!emitted) { outFailure = SpecializationFailure::ModuleRejected; ResultInfo r; r.errc = -10; r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" + String(session.GetLastErrorString()); return std::unexpected(r); } specialized.glsl = String(emitted); return specialized; } Result ShaderCompiler::DecompileShader(SpvcSession& session) { spvc_compiler_options options; session.CreateOptions(&options); spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); session.SetOptions(options); // Same ESSL spelling of the shading-rate built-ins DirectGLES emits. const Bool restoreShadingRateBuiltins = session.PrepareShadingRateBuiltinsForEssl(); const char* result = nullptr; session.Compile(&result); if (!result) { ResultInfo r; r.log += "Failed to compile the shader to GLSL: \n"; r.log += session.GetLastErrorString(); r.errc = -5; return std::unexpected(r); } std::string glsl = result; if (restoreShadingRateBuiltins) { SpvcSession::FinishShadingRateBuiltinsForEssl(glsl); } return glsl; } } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL