[Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively

This commit is contained in:
2026-08-22 00:57:06 -04:00
parent e4f41e0fd3
commit d4247db6c3
23 changed files with 761 additions and 181 deletions
+37 -27
View File
@@ -51,19 +51,21 @@ namespace MobileGL::MG_Util::SelfTest {
};
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
// a config flag rather than on anything either backend probes: the demotion is what
// makes doubles work, but GL_ARB_gpu_shader_fp64 promises the PRECISION the demotion
// cannot deliver, so the string is opt-in and the row has to say which way it went.
// a config flag rather than on anything either backend probes: doubles WORK on every
// backend, but GL_ARB_gpu_shader_fp64 additionally promises 64-bit PRECISION, which only
// a backend that consumes fp64 natively actually has. The string is opt-in either way -
// advertising it is a decision about the whole extension's surface, not just about
// precision - so the row has to say which way it went.
String AppendFp64AdvertisementNote(String detail) {
if (MG_Config::Features.AdvertiseFp64) {
return Move(detail) +
". GL_ARB_gpu_shader_fp64 IS advertised (MOBILEGL_ADVERTISE_FP64): an application "
"that checks the string will believe it has 64-bit precision, and it does not";
"that checks the string will believe it has 64-bit precision, which is true only "
"where the row above says native";
}
return Move(detail) +
". GL_ARB_gpu_shader_fp64 is not advertised, because the precision it promises is the "
"one thing the demotion cannot provide; set MOBILEGL_ADVERTISE_FP64=1 to advertise it "
"anyway";
". GL_ARB_gpu_shader_fp64 is not advertised by default; set MOBILEGL_ADVERTISE_FP64=1 "
"to advertise it anyway";
}
struct ReportBuilder {
@@ -2321,27 +2323,35 @@ namespace MobileGL::MG_Util::SelfTest {
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
"one reads nothing and glFramebufferTextureLayer on one is declined");
}
// Reported whichever way the device answers, because MobileGL no longer follows the
// device here: every 64-bit float is narrowed to 32 bits before any module reaches this
// backend (DemoteFloat64Pass), so the Float64 capability is never declared and a device
// that HAS the feature gains nothing from it. The device's own answer is still worth
// printing - it is the reason the demotion is unconditional.
builder.Pass("fp64", AppendFp64AdvertisementNote(
format("demoted to fp32 (device shaderFloat64 = {}) - every double / dvec / "
"dmat in a shader is narrowed to 32 bits before pipeline creation, so "
"such shaders BUILD AND RUN at single precision on every device "
"instead of failing to create a shader module on the ones without the "
"feature. A block containing a double is re-laid-out for the narrowed "
"members, so an application that hard-codes std140 offsets computed "
"for doubles must query them instead",
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
// and Mali both report VK_FALSE, so the demoted row is what a real phone prints; lavapipe
// reports VK_TRUE and gets real doubles.
if (features.shaderFloat64 == VK_TRUE) {
builder.Pass("fp64", AppendFp64AdvertisementNote(
"native (device shaderFloat64 = supported) - every double / dvec / dmat in "
"a shader keeps its declared width, blocks keep the layout glslang computed "
"for them, and glUniform*d stores 8-byte components. The one exception is a "
"VERTEX stage that declares a 64-bit float INPUT: there is no 64-bit vertex "
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
"device without the feature"));
} else {
builder.Pass("fp64", AppendFp64AdvertisementNote(
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
"shaders BUILD AND RUN at single precision instead of failing to create a "
"shader module. A block containing a double is re-laid-out for the narrowed "
"members, so an application that hard-codes std140 offsets computed for "
"doubles must query them instead"));
}
builder.Warn("64-bit vertex attributes",
"narrowed to float32; there is no 64-bit shader input left to feed after the fp64 "
"demotion above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most "
"devices anyway. glVertexAttribLFormat succeeds, its state is queryable, and an "
"ENABLED 64-bit array IS fetched - the source doubles are deinterleaved into a "
"float32 stream at draw, so values outside float32's range or precision are "
"rounded rather than exact");
"narrowed to float32 on every device, whatever the row above says: there is no "
"VK_FORMAT_R64*_SFLOAT vertex fetch here, and the format is chosen from the VAO "
"attribute, which does not know what type the shader declared - which is why a "
"vertex stage with a 64-bit float INPUT is narrowed whole even where fp64 is native. "
"glVertexAttribLFormat succeeds, its state is queryable, and an ENABLED 64-bit array "
"IS fetched - the source doubles are deinterleaved into a float32 stream at draw, so "
"values outside float32's range or precision are rounded rather than exact");
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
+18 -2
View File
@@ -117,8 +117,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// preprocessed text is in the L1 key verbatim, a strictly finer discriminator
// than the extension list. (E_GL_ARB_gpu_shader_fp64 is never read by the front
// end at all: MOBILEGL_ADVERTISE_FP64 only adds it to the extension STRING the
// application queries, and DemoteFloat64Pass runs unconditionally either way, so
// fp64 GLSL translates identically with the flag on or off.)
// application queries, and glslang parses `double` the same way either way.)
// * params.SupportsShaderFloat64, i.e. ConsumesFloat64Natively(). glslang produces
// the SAME SPIR-V under it - a `double` parses, reflects and generates as a
// 64-bit float regardless - so it is not a front-end input and putting it here
// would also cost L1c (the parse-verdict memo, which keys on this fingerprint and
// is genuinely independent of it) a false miss per backend. It DOES change what
// SanitizeAndOptimizeBinary produces, and L1's payload is post-Sanitize, so it
// rides in L1's key as a field of its own; see SpirvTranslationKeyInputs.
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
// the backends, never by the parse, the link or reflection.
// * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
@@ -135,6 +141,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; }
// Whether the backend this env was captured against can CONSUME a module that still
// declares 64-bit floats - the one thing that decides whether the transpile keeps
// `double` or narrows it (FlattenFloat64StorageBlockPass + DemoteFloat64Pass).
//
// The no-backend case answers FALSE, deliberately opposite to IsExtensionAdvertised's
// permissive fallback: an extension the frontend cannot gate against is best assumed
// present, but a hardware capability nothing has declared must be assumed absent. The
// demoted module is the one that works everywhere, so it is what a standalone compile
// (an internal shader object, a unit test) gets.
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
// Matches the historical rule exactly: with no active backend every extension counts
// as advertised, because the frontend then has nothing to gate against.
Bool IsExtensionAdvertised(GLExtension extension) const {
@@ -701,6 +701,57 @@ namespace MobileGL {
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;
@@ -751,7 +802,8 @@ namespace MobileGL {
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool validateOutput,
const bool enableSpirvValidation) {
const bool enableSpirvValidation,
const bool nativeFloat64) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
@@ -788,16 +840,25 @@ namespace MobileGL {
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
// No mobile GPU has 64-bit floats: Adreno and Mali both report shaderFloat64 ==
// VK_FALSE, and ESSL has no fp64 type for SPIRV-Cross to emit. Demoting here - in
// the one chain every module goes through, on both backends, at link - is what
// makes `double` compile at all, and makes it behave the SAME everywhere, which
// matters because the GL frontend's uniform storage cannot be per-backend: the
// glUniform*d shadow narrows to float unconditionally 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.
// 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
@@ -806,10 +867,16 @@ namespace MobileGL {
// 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.
optimizer.RegisterPass(
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass());
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
// 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);
@@ -23,10 +23,22 @@ namespace MobileGL {
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
static Result<SharedPtr<glslang::TProgram>> LinkProgram(const ProgramAttrib& attrib);
static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
// `nativeFloat64` is the caller's FINAL verdict, not a capability read: true means
// the two fp64 passes at the tail of the chain are skipped and real doubles reach
// the driver. False - which is DirectGLES always, every mobile device, and the
// no-backend default - runs the chain exactly as it always has. It is the ONE
// argument of this function that changes the output bytes, which is why it is
// also a field of the L1 memo's key.
//
// Production sets it in ProgramSpirvTask::GenerateSpirv, which takes the verdict
// for the WHOLE program (CompileEnv::ConsumesFloat64Natively() minus the
// 64-bit-vertex-input exception) before touching any module. Do not re-derive it
// per module: the global UBO is one buffer every stage reads.
static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool validateOutput = true,
bool enableSpirvValidation = false);
bool enableSpirvValidation = false,
bool nativeFloat64 = false);
// Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
// (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
// Only for backends without native draw-parameter support (DirectGLES).
@@ -432,6 +444,18 @@ namespace MobileGL {
// what the backends report: no mobile driver can build such a module.
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
// True when the module is a VERTEX stage that declares a 64-bit float INPUT
// variable - `in double`, `in dvec2`, `in dmat3` and so on.
//
// Asked only on a backend with native fp64, and it is what keeps that backend's
// vertex path consistent. No backend here can FETCH 64 bits (VK_FORMAT_R64*_SFLOAT
// is optional and lavapipe advertises none of them), and the format is chosen from
// the VAO attribute, which does not know what the shader declared - so a module
// that keeps a Float64 input would be fed a narrowed float32 stream, or a packed
// uint pair with no matching format. Such a module is demoted WHOLE instead, which
// is exactly what every other backend does to it.
static Bool ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv);
// True when the module declares an Input variable carrying a Location - i.e. a
// user-defined varying or a per-patch input, as opposed to a built-in.
//
@@ -29,6 +29,16 @@ namespace MobileGL {
// Espryt path never even reaches the driver. Demotion is what makes `double` in an
// application's GLSL compile and run everywhere, at fp32 precision.
//
// WHEN IT RUNS AT ALL. This pass is CAPABILITY-GATED at its one production caller,
// ShaderCompiler::SanitizeAndOptimizeBinary: a backend that can consume Float64 itself
// (DynamicBackendParameters::SupportsShaderFloat64, i.e. shaderFloat64 on DirectVulkan
// - lavapipe today and nothing else) skips it, and the module keeps its doubles.
// DirectGLES can never qualify, and neither can any real mobile device, so everything
// below still describes what happens there - which is everywhere that ships. The one
// exception that survives the capability: a VERTEX stage declaring a 64-bit float
// INPUT demotes the whole program regardless, because no backend here can FETCH 64
// bits (see ProgramSpirvTask::GenerateSpirv).
//
// BLOCK LAYOUT IS RE-DERIVED, NOT PRESERVED, and that was not the first choice - see
// BlockRelayout in the .cpp for the measurement that forced it. Preserving the 64-bit
// offsets (float + 4 bytes of padding in each slot) keeps the application's byte layout
@@ -67,9 +77,11 @@ namespace MobileGL {
// Index 0 of the same case PASSES by accident, for the same reason - writing 0.0f into
// the low half of 1.0 leaves it unchanged - so a partial pass here is not progress.
// Fixing it means carrying a double in the DEFAULT UNIFORM block without re-deriving
// its layout, and that block's routing is built by reflecting the module this pass
// produces, so the representation change ripples into every glUniform*d. Deliberately
// not attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
// its layout - which is precisely what the capability gate now does where the backend
// allows it: fp64-case1 PASSES on DirectVulkan/lavapipe (measured) and still fails on
// Espryt and on every device without shaderFloat64, where this pass runs. There is no
// fix for the demoted path itself; the value simply does not fit.
// compute_shader.fp64-case2 passes in both regimes and any attempt has to keep it
// green.
//
// SHADER STORAGE BLOCKS ARE NO LONGER IN THAT LIST, and the two cases that used to be
@@ -18,7 +18,13 @@ namespace MobileGL {
// Rewrites a SHADER STORAGE BLOCK that contains a 64-bit float into a flat
// `uint` word array, and turns every access to it into address arithmetic over
// that array. The application's byte layout survives exactly; the VALUES are
// still narrowed to 32-bit floats, because that is all any target here has.
// still narrowed to 32-bit floats, because that is all the target has.
//
// Registered ONLY on the demoting path, immediately before DemoteFloat64Pass, and
// capability-gated with it (ShaderCompiler::SanitizeAndOptimizeBinary). Where the
// backend consumes 64-bit floats itself there is no narrowing for this to preserve a
// layout across, and flattening a block the driver would have laid out correctly by
// itself would only cost the shader its index arithmetic.
//
// WHY THIS EXISTS. DemoteFloat64Pass rewrites `double` to `float` in place and
// lets SPIRV-Cross re-derive the block's packing from the declared types, because
@@ -30,7 +30,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// key (that map is an output of mapIO, not an input to it), and L1c's PAYLOAD gained
// the explicit uniform locations - so a blob written under 3 describes a differently
// shaped answer at both levels even where the bytes would have matched.
constexpr Uint32 kKeyLayoutVersion = 4u;
// 5: L1 gained nativeFloat64. SanitizeAndOptimizeBinary's fp64 tail is now capability-
// gated, so one L1 key shape can describe two materially different module sets (real
// doubles vs demoted-and-flattened) and a blob written under 4 says nothing about
// which one it holds.
constexpr Uint32 kKeyLayoutVersion = 5u;
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
@@ -123,6 +127,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(inputs.frontendFingerprint);
builder.Value(inputs.shaderCompileFlags);
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
builder.Value(static_cast<Uint8>(inputs.nativeFloat64));
builder.Value(static_cast<Uint64>(inputs.stages.size()));
for (const auto& stage : inputs.stages) {
builder.Value(static_cast<Uint32>(stage.type));
@@ -341,22 +341,26 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
//
// The cached artifact is the module AFTER SanitizeAndOptimizeBinary, not the
// raw GlslangToSpv output. That is a deliberate choice and it is safe:
// SanitizeAndOptimizeBinary is a fixed 11-pass spirv-opt chain with no
// arguments but the module, and its two remaining parameters (`validateOutput`,
// `enableSpirvValidation`) only decide whether the OUTPUT is handed to the
// validator and logged - RunOptimizerChecked runs the optimizer first and
// identically either way. Nothing between GlslangToSpv and Sanitize reads
// backend state. So caching after Sanitize saves the 96 us/stage the chain
// costs on top of the 40 us GlslangToSpv, and gives the backends exactly the
// bytes they would have got.
// SanitizeAndOptimizeBinary is a fixed spirv-opt chain whose only
// output-changing argument is `nativeFloat64` (below), and whose two other
// parameters (`validateOutput`, `enableSpirvValidation`) only decide whether
// the OUTPUT is handed to the validator and logged - RunOptimizerChecked runs
// the optimizer first and identically either way. Nothing between GlslangToSpv
// and Sanitize reads backend state. So caching after Sanitize saves the 96
// us/stage the chain costs on top of the 40 us GlslangToSpv, and gives the
// backends exactly the bytes they would have got.
//
// L1 IS BACKEND-AGNOSTIC BY CONTRACT. Two contexts on different GPUs compiling
// the same GLSL share one L1 entry: nothing that merely steers a BACKEND
// transpile (backend identity, GLES/Vulkan capability bits, driver extension
// strings, GPU vendor) is allowed in this key - all of that lives in L2's key,
// where it belongs. What IS here is the subset of the environment that changes
// what glslang itself produces; see CompileEnv::frontendFingerprint for the
// field-by-field classification and the evidence behind each call.
// L1 IS BACKEND-AGNOSTIC BY CONTRACT, WITH EXACTLY ONE DECLARED EXCEPTION.
// Two contexts on different GPUs compiling the same GLSL share one L1 entry:
// nothing that merely steers a BACKEND transpile (backend identity, GLES/Vulkan
// capability bits, driver extension strings, GPU vendor) is allowed in this key
// - all of that lives in L2's key, where it belongs. What IS here is the subset
// of the environment that changes what glslang itself produces (see
// CompileEnv::frontendFingerprint for the field-by-field classification), PLUS
// `nativeFloat64`, the one capability bit that reaches INSIDE
// SanitizeAndOptimizeBinary and therefore changes the cached bytes themselves.
// A capability bit belongs in this key if and only if it does that; anything
// that only changes what a backend does with the finished module still does not.
//
// WHAT IS IN THE KEY (each one is an input that can change the modules):
// * CompileEnv::frontendFingerprint - the glslang resource limits
@@ -376,7 +380,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the ShaderCompileBits the parse ran under (always 0 in production; in
// the key so a future non-zero value cannot alias);
// * the SPIR-V validation switch (byte-identical output either way, but it
// costs one byte to be sure).
// costs one byte to be sure);
// * nativeFloat64 - CompileEnv::ConsumesFloat64Natively(). The fp64 tail of
// SanitizeAndOptimizeBinary (FlattenFloat64StorageBlockPass +
// DemoteFloat64Pass) is skipped when the backend can build a pipeline from
// a module that still declares OpCapability Float64, so the SAME GLSL
// produces MATERIALLY DIFFERENT modules under the two answers - one with
// real doubles, one narrowed to 32 bits with its storage blocks flattened.
// Not folded into frontendFingerprint on purpose: glslang produces the same
// thing either way, so it is not a front-end input, and L1c shares that
// fingerprint and would take a false miss per backend for nothing.
//
// The key is a PROGRAM-level key, not a per-stage one, and that is forced:
// glslang's mapIO resolves a fragment stage's input Locations against the
@@ -398,6 +411,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
Uint32 shaderCompileFlags = 0;
Bool enableSpirvValidation = false;
// CompileEnv::ConsumesFloat64Natively() - the fp64 tail of the sanitize chain. The
// one backend capability bit in this key; see the note above for why it has to be.
Bool nativeFloat64 = false;
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
// When the payload was SPIR-V alone these were provably irrelevant: transform
// feedback is resolved by READING the linked intermediates and never writes an XFB