Files
MobileGL/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
T

2459 lines
153 KiB
C++

// 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 <algorithm>
#include <format>
#include <cmath>
#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 <MG_Backend/BackendObjects.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
#include <atomic>
#include <cctype>
#include <cstdlib>
#include <mutex>
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<int>(maxWorkGroupSize[0]);
Resources.maxComputeWorkGroupSizeY = static_cast<int>(maxWorkGroupSize[1]);
Resources.maxComputeWorkGroupSizeZ = static_cast<int>(maxWorkGroupSize[2]);
Resources.maxComputeWorkGroupCountX = static_cast<int>(maxWorkGroupCount[0]);
Resources.maxComputeWorkGroupCountY = static_cast<int>(maxWorkGroupCount[1]);
Resources.maxComputeWorkGroupCountZ = static_cast<int>(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<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
const String& source,
Flags<ShaderCompileBits> flags,
const CompileEnv* env) {
SharedPtr<glslang::TShader> res;
auto& tshader = res;
tshader = MakeShared<glslang::TShader>(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<EsOrigin> 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<SharedPtr<glslang::TShader>> 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<GLenum>(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<int>& dimensions,
const long long baseLocation,
UnorderedMap<String, Int>& 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<Int>(std::min(baseLocation + element * innerSpan,
static_cast<long long>(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<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader) {
UnorderedMap<String, Int> 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<int> 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<Int>(qualifier.layoutLocation);
locations.emplace(name, location);
RecordArrayOfArraysElementLocations(name, arraySizes, location, locations);
}
return locations;
}
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
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<glslang::TIoMapResolver> resolver;
UniquePtr<TMglGlslIoResolver> resolver;
for (unsigned stage = 0; stage < EShLangCount; stage++) {
if (program->getIntermediate((EShLanguage)stage) == nullptr) continue;
resolver =
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
attrib.explicitFragmentOutLocations,
attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings,
attrib.storageBlocksWithoutBinding,
attrib.uniformBlocksWithoutBinding,
&attrib.resourceBindingLimits,
attrib.resourceBindingViolation);
break;
}
auto ioMapper = UniquePtr<glslang::TIoMapper>(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<Vector<Vector<unsigned>>> ShaderCompiler::GetSpirvBinaryFromProgram(
const ProgramBinaryAttrib& attrib) {
glslang::SpvOptions spvOptions;
spvOptions.disableOptimizer = false;
Vector<Vector<unsigned>> allSpirv;
for (auto type : attrib.shaderTypes) {
Vector<unsigned> 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<Uint64> 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<Uint32> 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<Uint32>& 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<unsigned long long>(
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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& 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<spvtools::opt::IRContext> 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<spv::Dim>(type.GetSingleWordInOperand(1)) == spv::Dim::Buffer) {
return true;
}
}
return false;
}
Bool ShaderCompiler::ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> 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<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
spv::ExecutionMode::Xfb) {
return true;
}
}
return false;
}
Bool ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> 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<spv::Capability>(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<Uint32>(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<Uint32>& spirv) {
PointSizeModuleProbe probe;
if (spirv.empty()) {
probe.parsed = true; // an absent stage constrains nothing
return probe;
}
std::unique_ptr<spvtools::opt::IRContext> 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<spv::Capability>(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<spv::ExecutionModel>(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<Uint32> patchDecorated;
for (spvtools::opt::Instruction& annotation : context->annotations()) {
if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 2 &&
static_cast<spv::Decoration>(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<spv::Decoration>(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<spv::StorageClass>(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<spv::Decoration>(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<Vector<Uint32>>& modules, const Vector<GLenum>& 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<Int>(i); break;
case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast<Int>(i); break;
case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast<Int>(i); break;
case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast<Int>(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<Vector<Uint32>> 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<Uint32>& 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<spvtools::opt::IRContext> 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<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> 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<spv::ExecutionModel>(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<spv::StorageClass>(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<Uint32>& spirv) {
if (spirv.empty()) {
return false;
}
std::unique_ptr<spvtools::opt::IRContext> 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<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
Bool located = false;
context->get_decoration_mgr()->ForEachDecoration(
variable.result_id(), static_cast<uint32_t>(spv::Decoration::Location),
[&located](const spvtools::opt::Instruction&) { located = true; });
if (located) return true;
}
return false;
}
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& binary) {
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
}
bool ShaderCompiler::ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& binary) {
return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary);
}
ShaderCompiler::SpirvGateFeatures ShaderCompiler::ProbeSpirvGateFeatures(
const Vector<Uint32>& binary) {
SpirvGateFeatures features;
if (binary.empty()) {
return features;
}
std::unique_ptr<spvtools::opt::IRContext> 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& 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<Uint32>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& 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<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
Vector<uint32_t>& 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<String>& flattenedBlockNames, String& outName) {
return FlattenXfbInterfaceBlocksPass::RewriteCaptureName(captureName, flattenedBlockNames,
outName);
}
void ShaderCompiler::ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
std::set<String>& collidingBlockNames,
std::set<String>& 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<spvtools::opt::IRContext> 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<Uint32>& inputBinary,
const std::map<String, String>& inputBlockRenames,
const std::map<String, String>& outputBlockRenames,
std::set<String>& renamedBlockNames,
Vector<uint32_t>& 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<Uint32>& inputBinary,
const bool stripInputBlocks,
const bool stripOutputBlocks,
bool& strippedAny,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<uint32_t> 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<Uint32>& inputBinary, Vector<uint32_t>& 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<uint32_t> 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<Uint32>& inputBinary, Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary, Vector<uint32_t>& 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<Uint32>& inputBinary, Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
constexpr SizeT kSpirvHeaderWordCount = 5;
outputBinary.clear();
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
return false;
}
Vector<Uint32> floatTypeIds;
Vector<Uint32> resultTypeById(inputBinary[3], 0);
Vector<Uint32> 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<spv::Op>(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<spv::Capability>(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<Uint32> atomicImageTypeIds;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
const Uint32 instructionWord = inputBinary[offset];
const Uint32 wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(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<spv::Op>(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<Uint32>(spv::ImageFormat::Unknown);
hasFloatStorageImage = true;
}
}
offset += wordCount;
}
if (!hasFloatStorageImage) {
return true;
}
Vector<Uint32> addedCapabilities;
const Uint32 capabilityInstruction =
(2u << 16u) | static_cast<Uint32>(spv::Op::OpCapability);
if (!hasReadWithoutFormatCapability) {
addedCapabilities.push_back(capabilityInstruction);
addedCapabilities.push_back(
static_cast<Uint32>(spv::Capability::StorageImageReadWithoutFormat));
}
if (!hasWriteWithoutFormatCapability) {
addedCapabilities.push_back(capabilityInstruction);
addedCapabilities.push_back(
static_cast<Uint32>(spv::Capability::StorageImageWriteWithoutFormat));
}
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(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<Uint32> 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<String> BuildXfbVaryingRequest(const Vector<SpirvXfbCapture>& captures) {
Vector<String> names;
if (captures.empty()) return names;
auto emitSkip = [&names](Uint32 components) {
while (components > 0) {
const Uint32 step = std::min<Uint32>(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<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& 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::SpecializedModule> ShaderCompiler::SpecializeAndDecompileSpirvModule(
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds, const Vector<Uint32>& 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<String> 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