Files
MobileGL/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h
T

187 lines
13 KiB
C++

// MobileGL - MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h
// 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
#pragma once
#include <Includes.h>
#include <Config.h>
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE
// place because three separate readers have to agree on them: CaptureCompileEnv (which floors
// the backend's answer at them), GL_Getter (which answers the same query the same way) and
// BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against
// the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies,
// and the z one disagreed: glslang compiled against 1024 while the context advertised 64.
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535};
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number
// glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to.
inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024;
// everything outside (stage, source) this reads - advertised extensions and backend limits -
// so the transformation is a pure function of its three arguments and can run on a worker
// thread.
//
// Why it exists (P1): every one of those reads is a reach-back into
// MG_Backend::pActiveBackendObject / gBackendFunctionsTable, and one of them
// (GL_MAX_COMPUTE_WORK_GROUP_SIZE) is a *real driver call* that on the DirectGLES
// backend silently no-ops off the context thread - which would turn a perfectly legal
// `local_size_z` into COMPILE_STATUS=FALSE the moment compilation moved to a worker.
// Snapshotting the whole set once per context, on the GL thread, removes every
// reach-back at once and makes the pipeline a pure function of (stage, source, env).
//
// Lifetime: captured lazily on first use by GLState::GLContext::GetCompileEnv(), and
// RE-captured if the active backend object changes. Immutable once published; held by
// value/`SharedPtr<const CompileEnv>` so a worker can never observe a torn update.
//
// Memo-hazard rule: `fingerprint` hashes every member above it and is part of the P0b
// ShaderPreprocessCache key, so a memo computed against one env can never be returned
// against another. ADDING A FIELD HERE MEANS ADDING IT TO ComputeFingerprint().
struct CompileEnv {
// --- compute limits: the ONLY former real-driver read in the pipeline ---
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1],
MIN_COMPUTE_WORK_GROUP_SIZE[2]};
// GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is:
// gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline
// needs the number the context advertises without reaching back to the live backend.
Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1],
MIN_COMPUTE_WORK_GROUP_COUNT[2]};
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
Uint64 maxComputeWorkGroupInvocations = 1024;
// --- backend identity + limits ---
// Unknown means "no backend was active at capture time". Every consumer keeps the
// exact no-backend fallback it had before: extensions read as advertised, limits
// read as the frontend defaults.
BackendType backend = BackendType::Unknown;
MG_Backend::DynamicBackendParameters params{}; // by value, never by reference
Vector<GLExtension> advertisedExtensions;
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
// The FRONT-END half of the environment: the subset of the fields above that can
// change what glslang PRODUCES - the SPIR-V or the reflection - as opposed to what a
// BACKEND later does with the result. This, and never `fingerprint`, is what the L1
// shader translation memo keys on, because L1 is backend-agnostic BY CONTRACT: two
// contexts on different GPUs compiling the same GLSL must share one L1 entry.
//
// THE LINE THIS DRAWS. "Backend-agnostic" means BACKEND IDENTITY is out - the vendor,
// the extension list, which of DirectGLES/DirectVulkan is active, every capability bit
// that merely steers the transpile. It does NOT mean backend-DERIVED VALUES are out: a
// resource limit that glslang enforces at parse, or expands into a built-in constant,
// is a front-end INPUT no matter where the number came from, and dropping it would be
// a silent miscompile rather than a backend leak. A driver with 16 vertex attribs and
// one with 32 genuinely reflect the same GLSL differently.
//
// WHAT IS IN IT (audited; re-audit whenever a new env read appears in the front end):
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. glslang
// enforces those at parse, so they decide whether a shader compiles at all and can
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d),
// the third time in three waves that a hardcoded TBuiltInResource field became
// env-derived - assume the next wave does it again and re-audit.
// * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each.
// These moved IN at the dev merge that brought wave3's cb155c5b, which made
// BuildTBuiltInResource read them from the env instead of hardcoding a permissive
// cap - exactly the migration the old exclusion note said would force them in
// here. They are not merely a reject gate: glslang expands both into built-in
// CONSTANTS (gl_MaxComputeWorkGroupSize, gl_MaxComputeWorkGroupCount), so a
// compute module that reads one generates different SPIR-V under two drivers that
// report different numbers.
// * MaxVertexAttribs and the HasBackend() bit: the two inputs to ProgramLinkTask's
// GetReflectionVertexAttribLimit, which bounds how many vertex input locations
// reflection records - so they change the REFLECTION the memo carries.
//
// The sharding this costs is nil in practice and worth naming so nobody re-litigates
// it: a process has ONE active backend at a time and CompileEnv is re-captured when
// that changes, so no live run ever has two of these fingerprints competing for the
// same L1 entries. The cost would only appear on a future cross-device DISK tier,
// where it is the correct cost - those devices really do compile that GLSL differently.
//
// WHAT IS DELIBERATELY OUT:
// * `backend` beyond the HasBackend() bit. Nothing in the parse, the link or
// GlslangToSpv branches on which backend is active - ShaderAttrib::flags is 0 on
// both production parse paths (ShaderCompileTask::RunCompilePipeline and
// ClaimParsedShader). Backend identity steers the TRANSPILE, which is L2's key.
// * `advertisedExtensions`. Its only front-end consumer is ShaderSourceProcessor's
// FilterUnsupportedGpuShaderInt64, which REWRITES THE SOURCE TEXT - and the
// 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 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
// moved into the list above at the wave3 merge. glslang has no
// gl_MaxComputeWorkGroupInvocations built-in and BuildTBuiltInResource does not
// read this field, so its sole consumer is still ValidateComputeLocalSizeLimits, a
// pre-parse ACCEPT/REJECT gate. A rejected shader fails its compile, so its
// program never reaches the tail of the link and no L1 entry is ever created under
// a rejecting environment; an accepted one parses identically at any value.
// THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE, and it is now the
// only such argument left in this classification. The moment anything hands this
// value to glslang - a TBuiltInResource field, a built-in constant - it MUST move
// into the fingerprint, exactly as its companions just did.
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 {
if (!HasBackend()) return true;
return std::find(advertisedExtensions.begin(), advertisedExtensions.end(), extension) !=
advertisedExtensions.end();
}
};
// Hashes every semantically relevant member. Public so a test can assert that two
// different envs really do produce different P0b cache keys.
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
// The backend-agnostic half; see CompileEnv::frontendFingerprint for the classification
// and the evidence behind each call. Public so a test can assert both directions: that a
// backend-only difference produces the SAME value (which is what pins L1's
// backend-agnosticism) and that a front-end limit produces a different one.
Uint64 ComputeFrontendCompileEnvFingerprint(const CompileEnv& env);
// GL thread only: this is where the GL_MAX_COMPUTE_WORK_GROUP_SIZE queries live now.
SharedPtr<const CompileEnv> CaptureCompileEnv();
// The env a context-less caller gets: exactly what CaptureCompileEnv() would produce
// with no active backend. Used by the unit tests that drive the transpiler directly and
// by the internal shader objects that compile before any context exists.
const SharedPtr<const CompileEnv>& GetDefaultCompileEnv();
// The env of the current GL context, or GetDefaultCompileEnv() when there is none.
// GL thread only (it may trigger a capture). This is the compatibility shim for the
// handful of entry points that still resolve their env implicitly; the pipeline itself
// always takes an explicit `const CompileEnv&`.
const SharedPtr<const CompileEnv>& GetCurrentCompileEnv();
} // namespace MobileGL::MG_Util::ShaderTranspiler