mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user