mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively
This commit is contained in:
@@ -787,9 +787,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// The L1 key. Every input below is one that can change the SPIR-V this program
|
||||
// generates; see the key inventory on SpirvTranslationKeyInputs.
|
||||
//
|
||||
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
|
||||
// Deliberately NOT keyed on: anything that only steers a BACKEND transpile - see the
|
||||
// classification on CompileEnv::frontendFingerprint, and L2's own key in
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h. The single capability bit that IS here
|
||||
// (nativeFloat64) earns its place by changing SanitizeAndOptimizeBinary's own output,
|
||||
// which is what the payload stores.
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
|
||||
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
@@ -805,6 +807,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// value cannot alias a module parsed without it.
|
||||
keyInputs.shaderCompileFlags = 0;
|
||||
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
|
||||
// The one BACKEND capability bit in this key, and it has to be here: it reaches inside
|
||||
// SanitizeAndOptimizeBinary, whose output is what the payload holds. Read from the same
|
||||
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
|
||||
// disagree.
|
||||
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
|
||||
keyInputs.stages.reserve(in.shaders.size());
|
||||
for (const LinkShaderInput& shader : in.shaders) {
|
||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||
|
||||
@@ -155,6 +155,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
||||
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
||||
// Read straight off m_spirv, not through UsesNativeFloat64(): this runs INSIDE the
|
||||
// phase-B publish, where the join gate is not re-entrant. Same reason the scratch above
|
||||
// is taken directly.
|
||||
const Bool nativeFloat64 = m_spirv.nativeFloat64;
|
||||
|
||||
for (const auto& init : initializers) {
|
||||
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
|
||||
@@ -165,12 +169,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
const Int elements = init.arraySize;
|
||||
if (componentsPerElement <= 0 || elements <= 0) continue;
|
||||
|
||||
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
|
||||
// float in a shader is narrowed to 32 bits before the module reaches a backend
|
||||
// EbtDouble belongs with the floats, not with the skipped types. On a DEMOTED
|
||||
// program its 64-bit floats were narrowed to 32 before the module reached a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
|
||||
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
|
||||
// value into floatValues, which is a vector<double> either way. Leaving it out meant
|
||||
// the initializer was silently dropped and the uniform came up zero.
|
||||
// the 32-bit shadow encoding a `uniform float` does; on a program that kept them it
|
||||
// has an 8-byte one, which the store width below picks up. glslang folded the value
|
||||
// into floatValues, a vector<double>, in both cases. Leaving it out meant the
|
||||
// initializer was silently dropped and the uniform came up zero.
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat ||
|
||||
init.basicType == glslang::EbtFloat16 ||
|
||||
init.basicType == glslang::EbtDouble;
|
||||
@@ -195,22 +200,36 @@ namespace MobileGL::MG_State::GLState {
|
||||
// std140 pads every column of a float matrix out to a vec4, so the columns of
|
||||
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span
|
||||
// states the stride the rest of the pipeline agreed on rather than guessing it.
|
||||
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location));
|
||||
// The static form, with the width taken from m_spirv directly: the member
|
||||
// overload asks UsesNativeFloat64(), which joins phase B - and phase B is what
|
||||
// is publishing right now.
|
||||
const SizeT slotSpan =
|
||||
UniformStorageSpanInBytes(GetUniformTypeFacts(static_cast<Uint>(location)),
|
||||
GetUniformSizesInBytes(static_cast<Uint>(location)), nativeFloat64);
|
||||
const SizeT columnStride =
|
||||
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
|
||||
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
|
||||
const Int columnCount = columns > 0 ? columns : 1;
|
||||
|
||||
// A `double` initializer on a program that KEPT its doubles lands in an 8-byte
|
||||
// component, not a 4-byte one; every other basic type - and every double on a
|
||||
// demoted program - stays one 32-bit word. glslang folded the value into
|
||||
// floatValues (a vector<double>) either way, so only the store width moves.
|
||||
const Bool isWideDouble = init.basicType == glslang::EbtDouble && nativeFloat64;
|
||||
const SizeT componentSize = isWideDouble ? sizeof(Double) : sizeof(Uint32);
|
||||
for (Int column = 0; column < columnCount; ++column) {
|
||||
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
|
||||
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32);
|
||||
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * componentSize;
|
||||
if (byteOffset + writeSize > uboSize) break;
|
||||
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
|
||||
static_cast<SizeT>(column) * componentsPerColumn;
|
||||
for (Int component = 0; component < componentsPerColumn; ++component) {
|
||||
const SizeT source = firstComponent + static_cast<SizeT>(component);
|
||||
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32);
|
||||
if (isFloat) {
|
||||
Uint8* const destination = scratch + byteOffset + component * componentSize;
|
||||
if (isWideDouble) {
|
||||
const Double value = init.floatValues[source];
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
} else if (isFloat) {
|
||||
const Float value = static_cast<Float>(init.floatValues[source]);
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
} else {
|
||||
|
||||
@@ -565,26 +565,47 @@ namespace MobileGL::MG_State::GLState {
|
||||
: kInvalidUniformOffset;
|
||||
}
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
|
||||
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
|
||||
// shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
|
||||
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
|
||||
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
|
||||
// reading or writing a whole uniform's storage - a bounds check, a copy between two
|
||||
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) {
|
||||
if (type.isMatrix) {
|
||||
return static_cast<SizeT>(type.matrixCols) * 4 * sizeof(Float);
|
||||
// std140 column stride of a matrix uniform in the global UBO: every column is padded out
|
||||
// to the base alignment of a vec4 for 32-bit components, and of a dvec4 for 64-bit ones -
|
||||
// except that a 2-ROW double column is a dvec2, whose base alignment is already 16.
|
||||
// (GL 4.6 core 7.6.2.2 rules 2-4; SPIRV-Cross derives the same numbers, which is what
|
||||
// makes this agree with the reflected module.)
|
||||
static SizeT UniformMatrixColumnStride(const TypeFacts& type, const Bool nativeFloat64) {
|
||||
if (type.isDouble && nativeFloat64) {
|
||||
return type.matrixRows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
|
||||
}
|
||||
if (type.isDouble) {
|
||||
return 4 * sizeof(Float);
|
||||
}
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4 (or a dvec4), so a
|
||||
// mat3 spans 48 bytes even though only 36 of them carry components. And a 64-bit float
|
||||
// may have been narrowed to 32 before the module reached the backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting
|
||||
// whichever module was produced - so on a DEMOTED program a `double` uniform occupies
|
||||
// exactly what its float-typed twin would, half its GL type size, and a `dmat4` is padded
|
||||
// like any other 32-bit matrix. On a program that kept its doubles it occupies the full
|
||||
// GL type size and its matrix columns are twice as far apart. `nativeFloat64` is the
|
||||
// program's own SpirvArtifacts flag, never a live backend read: it describes the modules
|
||||
// that were actually built. Anything reading or writing a whole uniform's storage - a
|
||||
// bounds check, a copy between two programs' shadows - wants this rather than
|
||||
// GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize,
|
||||
const Bool nativeFloat64 = false) {
|
||||
if (type.isMatrix) {
|
||||
return static_cast<SizeT>(type.matrixCols) * UniformMatrixColumnStride(type, nativeFloat64);
|
||||
}
|
||||
if (type.isDouble && !nativeFloat64) {
|
||||
return tightSize / 2;
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
// Whether this program's modules KEPT their 64-bit floats. Joins phase B, like every
|
||||
// other question about the global UBO's layout - and it is one: it decides how wide a
|
||||
// `double` uniform's slot is.
|
||||
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location));
|
||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
|
||||
UsesNativeFloat64());
|
||||
}
|
||||
|
||||
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
|
||||
@@ -1323,6 +1344,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
|
||||
@@ -122,7 +122,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
|
||||
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
|
||||
artifacts.enableSpirvValidation = enableSpirvValidation;
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
// Whether this backend consumes 64-bit floats itself. Read off the SNAPSHOT, like every
|
||||
// other environment question this node asks: a worker may not touch
|
||||
// MG_Backend::pActiveBackendObject, and the answer has to be the one the L1 key was built
|
||||
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
|
||||
// one answer could be handed back under the other.
|
||||
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
||||
nativeFloat64);
|
||||
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
||||
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
||||
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
||||
@@ -181,7 +188,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation) {
|
||||
const Bool enableSpirvValidation, const Bool nativeFloat64) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -209,12 +216,39 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
|
||||
// The fp64 verdict, taken ONCE for the whole program and before any module is touched.
|
||||
//
|
||||
// Per program rather than per module, and that is forced by the global UBO: all stages
|
||||
// read one buffer whose layout is derived by reflecting the modules, so a vertex stage
|
||||
// that stored a `uniform double` as 4 bytes next to a fragment stage that stored it as 8
|
||||
// would put every uniform after it somewhere different in each, and the routing table
|
||||
// (one offset per location) could only describe one of them.
|
||||
//
|
||||
// The exception itself is the vertex INPUT: no backend here can fetch a 64-bit attribute,
|
||||
// and VertexInputStateFactory picks the format from the VAO attribute without ever seeing
|
||||
// what the shader declared, so a Float64 input would meet a narrowed float32 stream. One
|
||||
// such stage demotes the whole program, which is exactly what every backend without
|
||||
// native fp64 does to it anyway.
|
||||
Bool keepFloat64 = nativeFloat64;
|
||||
if (keepFloat64) {
|
||||
for (const auto& spv : artifacts.generatedSpirv) {
|
||||
if (ShaderCompiler::ModuleDeclaresFloat64VertexInput(spv)) {
|
||||
keepFloat64 = false;
|
||||
MGLOG_D("ProgramObject %u: a vertex stage declares a 64-bit float input; demoting the "
|
||||
"whole program despite native fp64",
|
||||
externalIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
artifacts.nativeFloat64 = keepFloat64;
|
||||
|
||||
// Linked SPIR-V generated, sanitize and optimize it
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation, keepFloat64);
|
||||
if (!success) {
|
||||
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
||||
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
void RunBody() override;
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
|
||||
Bool nativeFloat64);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
|
||||
Reference in New Issue
Block a user