[Refactor] (MG_State, MG_Util): join-by-construction link/compile artifacts (P1 stage 2)

Still fully synchronous - EnsureLinkJoined()/EnsureCompileJoined() are empty
inline no-ops (verified to fold away at every one of the ~1200 call sites;
this project builds without LTO) - but every read of link- or compile-produced
state now goes through a private accessor the compiler enforces, so when
stage 4 moves the bodies onto pool workers, 'which reads must join' is a
type-system fact instead of a 400-line audit.

- ProgramObject: the 31 fields ResetLinkArtifacts clears plus the 5 link
  outputs it forgot (infoLog, linkedFragData{Location,Index}, the geometry
  strip-capture pair) move into a nested LinkArtifacts behind Artifacts().
  ResetLinkArtifacts is now a worker-safe pure clear; the link-observable
  version bumps (backendState/link/uboContent) move to a GL-thread-only
  BumpLinkObservableVersions() called once from Link()'s prologue and from
  glProgramBinary's mandated failure - the link body never writes them, so
  a stage-4 worker cannot lose an invalidation against the draw path.
- ShaderObject: compile artifacts (TShader, preprocessed source, side-channel
  maps, status/log, consume-once flag) behind Compiled(); the P0b layer-1
  memo trio deliberately stays outside as the future non-joining
  COMPLETION_STATUS_KHR fast path.
- CompileEnv (new): a GL-thread snapshot of everything the compile pipeline
  used to read live from the backend mid-parse - compute limits (the
  GetIntegeri_v reach-back is gone from the worker path), advertised
  extensions, device quirks, TBuiltInResource inputs. Captured lazily per
  backend activation; the consume-once re-parse now runs against the same
  env as the original parse.
- The GL-thread prologue / worker-body boundary is marked in Link() where
  the stage sort ends; everything below is a pure function of the snapshot.

Public getter signatures unchanged - MG_Impl and both backends compile
untouched. Unit 476/476, Program suites 117/117, DirectGLES retrace 38/39 on
llvmpipe (the one failure is the known pre-existing non-CI iterationrp case;
the NVIDIA userspace driver was updated out from under the running kernel
module mid-session, so GLX there is down until a reboot).
This commit is contained in:
BZLZHH
2026-08-08 07:12:37 -04:00
parent 8191075133
commit c93e5fa409
18 changed files with 1186 additions and 654 deletions
+1
View File
@@ -190,6 +190,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
+14
View File
@@ -9,6 +9,8 @@
#include "Core.h"
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
#include "MG_State/EGLState/Core.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <Config.h>
namespace MobileGL::MG_State {
@@ -24,6 +26,18 @@ namespace MobileGL::MG_State {
}
namespace GLState {
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GLContext::GetCompileEnv() {
const void* backend = static_cast<const void*>(MG_Backend::pActiveBackendObject.get());
if (!m_compileEnv || m_compileEnvBackend != backend) {
// First use, or the backend was swapped underneath us. Re-capturing rolls the
// fingerprint, so every P0b preprocess memo computed against the old backend's
// limits becomes structurally unreachable instead of silently reusable.
m_compileEnv = MG_Util::ShaderTranspiler::CaptureCompileEnv();
m_compileEnvBackend = backend;
}
return m_compileEnv;
}
// Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
m_errorState.RecordError(code, Move(info));
+18
View File
@@ -21,6 +21,10 @@
#include "VertexArrayState/VertexArrayState.h"
#include "RenderbufferState/RenderbufferState.h"
namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv;
}
namespace MobileGL {
namespace MG_State {
void Init();
@@ -380,6 +384,15 @@ namespace MobileGL {
Bool ValidateRenderbufferName(Uint index) const;
Bool ValidateRenderbufferObject(Uint index) const;
// P1: the shader compile/link pipeline's snapshot of everything it reads from
// outside its own (stage, source) inputs. Captured lazily here because it
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
// so there is no backend to query yet. Re-captured whenever the active backend
// object changes, which also rolls the fingerprint and therefore invalidates
// every P0b preprocess memo keyed against the old one.
// GL thread only.
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
private:
// State Components
ErrorState m_errorState;
@@ -437,6 +450,11 @@ namespace MobileGL {
FramebufferState m_framebufferState;
SamplerState m_samplerState;
RenderbufferState m_renderbufferState;
mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv;
// Identity of the backend object m_compileEnv was captured against; a plain
// pointer compare, never dereferenced.
const void* m_compileEnvBackend = nullptr;
};
} // namespace GLState
File diff suppressed because it is too large Load Diff
@@ -44,28 +44,28 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
const String& GetInfoLog() const { return m_infoLog; }
const String& GetInfoLog() const { return Artifacts().infoLog; }
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
// is the only place a caller can read it from once the shader name is gone.
void AppendInfoLog(const String& text) {
if (text.empty()) return;
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n';
m_infoLog += text;
if (!Artifacts().infoLog.empty() && Artifacts().infoLog.back() != '\n') Artifacts().infoLog += '\n';
Artifacts().infoLog += text;
}
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; }
Uint GetUniformCount() const { return m_activeUniformCount; }
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
Int GetUniformMaxLength() const { return Artifacts().uniformNameMaxLength; }
Uint GetUniformCount() const { return Artifacts().activeUniformCount; }
Uint GetMaxUniformLocation() const { return Artifacts().maxUniformLocation; }
Int GetUniformLocation(const String& name) const {
const auto it = m_uniformLocations.find(name);
if (it != m_uniformLocations.end()) return (Int)it->second;
const auto it = Artifacts().uniformLocations.find(name);
if (it != Artifacts().uniformLocations.end()) return (Int)it->second;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
// to base + k because DoReflection reserves one location per array element.
if (name.empty()) return -1;
if (name.back() != ']') {
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
const auto suffixedIt = Artifacts().uniformLocations.find(name + "[0]");
if (suffixedIt != Artifacts().uniformLocations.end()) return (Int)suffixedIt->second;
return -1;
}
if (name.length() < 4) return -1;
@@ -78,19 +78,19 @@ namespace MobileGL::MG_State::GLState {
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1;
}
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
if (baseIt == m_uniformLocations.end()) {
auto baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket) + "[0]");
if (baseIt == Artifacts().uniformLocations.end()) {
// Legacy key without the "[0]" suffix (defensive; reflection normally
// stores the suffixed form for arrays).
baseIt = m_uniformLocations.find(name.substr(0, bracket));
if (baseIt == m_uniformLocations.end()) return -1;
baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket));
if (baseIt == Artifacts().uniformLocations.end()) return -1;
}
const Int base = (Int)baseIt->second;
if (!IsValidUniformLocation(base)) return -1;
const Int index = m_uniformIndexInTProgram[base];
const Int index = Artifacts().uniformIndexInTProgram[base];
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
// in-range elements.
const glslang::TType* type = m_program->getUniform(index).getType();
const glslang::TType* type = Artifacts().program->getUniform(index).getType();
if (type == nullptr || !type->isArray()) return -1;
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
const Int location = base + (Int)element;
@@ -101,7 +101,7 @@ namespace MobileGL::MG_State::GLState {
// True when both locations are element slots of the same uniform variable.
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
return Artifacts().uniformIndexInTProgram[a] == Artifacts().uniformIndexInTProgram[b];
}
// ---- GL index <-> glslang TProgram index translation ----
@@ -111,22 +111,22 @@ namespace MobileGL::MG_State::GLState {
// index spaces; every public "index"-taking getter translates through them, so
// GL and backend consumers keep seeing exactly the pre-P0a surface.
Int TProgramUniformIndex(Uint glIndex) const {
return m_glUniformIndexToTProgram[glIndex];
return Artifacts().glUniformIndexToTProgram[glIndex];
}
Int GlUniformIndexFromTProgram(Int tIndex) const {
if (tIndex < 0 || tIndex >= static_cast<Int>(m_tProgramUniformIndexToGl.size())) return -1;
return m_tProgramUniformIndexToGl[tIndex];
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
return Artifacts().tProgramUniformIndexToGl[tIndex];
}
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(m_tProgramBlockIndexToGl.size())) return -1;
return m_tProgramBlockIndexToGl[tBlockIndex];
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(Artifacts().tProgramBlockIndexToGl.size())) return -1;
return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
}
Int GetActiveUniformIndex(const String& name) const {
const Int tProgramCount = static_cast<Int>(m_tProgramUniformIndexToGl.size());
const Int uniformIndex = m_program->getUniformIndex(name.c_str());
const Int tProgramCount = static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str());
if (uniformIndex >= 0 && uniformIndex < tProgramCount &&
m_program->getUniform(uniformIndex).name == name) {
Artifacts().program->getUniform(uniformIndex).name == name) {
return GlUniformIndexFromTProgram(uniformIndex);
}
@@ -135,9 +135,9 @@ namespace MobileGL::MG_State::GLState {
// robustness against non-suffixed reflection entries.
if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]";
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str());
if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
m_program->getUniform(suffixedIndex).name == suffixedName) {
Artifacts().program->getUniform(suffixedIndex).name == suffixedName) {
return GlUniformIndexFromTProgram(suffixedIndex);
}
return -1;
@@ -145,28 +145,28 @@ namespace MobileGL::MG_State::GLState {
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
const String baseName = name.substr(0, name.length() - 3);
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
const Int baseIndex = Artifacts().program->getUniformIndex(baseName.c_str());
if (baseIndex < 0 || baseIndex >= tProgramCount) return -1;
return m_program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
: -1;
}
Bool IsValidUniformLocation(Int location) const {
if (location < 0 || location > static_cast<Int>(m_maxUniformLocation)) return false;
if (static_cast<SizeT>(location) >= m_uniformIndexInTProgram.size()) return false;
const Int uniformIndexInProgram = m_uniformIndexInTProgram[location];
if (location < 0 || location > static_cast<Int>(Artifacts().maxUniformLocation)) return false;
if (static_cast<SizeT>(location) >= Artifacts().uniformIndexInTProgram.size()) return false;
const Int uniformIndexInProgram = Artifacts().uniformIndexInTProgram[location];
return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd &&
uniformIndexInProgram >= 0 &&
uniformIndexInProgram < static_cast<Int>(m_tProgramUniformIndexToGl.size());
uniformIndexInProgram < static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
}
GLenum GetUniformType(Uint location) const {
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
return uniform.glDefineType;
}
GLenum GetActiveUniformType(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
return uniform.glDefineType;
}
@@ -174,9 +174,9 @@ namespace MobileGL::MG_State::GLState {
// glslang's TObjectReflection.size only carries the element count for a NON-block array; for
// a block array member it reports 1, so take the count from the TType, which is authoritative
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
// m_uniformIndexInTProgram stores).
// the artifacts' uniformIndexInTProgram stores).
GLint GetUniformArraySizeByTIndex(Int tIndex) const {
const auto& uniform = m_program->getUniform(tIndex);
const auto& uniform = Artifacts().program->getUniform(tIndex);
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isSizedArray()) {
return type->getOuterArraySize();
@@ -189,7 +189,7 @@ namespace MobileGL::MG_State::GLState {
}
Int GetActiveUniformBlockIndex(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
return GlBlockIndexFromTProgram(uniform.index);
}
@@ -198,7 +198,7 @@ namespace MobileGL::MG_State::GLState {
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
// seeing them as default-block uniforms, so gate on the GL-visible block index.
GLint GetActiveUniformOffset(Uint index) const {
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
return uniform.offset;
}
@@ -212,7 +212,7 @@ namespace MobileGL::MG_State::GLState {
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
// layout is always std140, where every array element stride rounds up to a vec4.
GLint GetActiveUniformArrayStride(Uint index) const {
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isArray()) return 0;
@@ -232,13 +232,13 @@ namespace MobileGL::MG_State::GLState {
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
// an inheriting member's layoutMatrix == ElmNone.
GLint GetActiveUniformIsRowMajor(Uint index) const {
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isMatrix()) return 0;
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
if (layoutMatrix == glslang::ElmNone) {
layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
}
return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0;
}
@@ -250,13 +250,13 @@ namespace MobileGL::MG_State::GLState {
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
// every GL 3.3 float matrix this evaluates to 16, independent of majorness.
GLint GetActiveUniformMatrixStride(Uint index) const {
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isMatrix()) return 0;
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
if (layoutMatrix == glslang::ElmNone) {
layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
}
const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
@@ -268,50 +268,50 @@ namespace MobileGL::MG_State::GLState {
}
const glslang::TType* GetUniformTType(Uint location) const {
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
return uniform.getType();
}
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); }
const String& GetUniformName(Uint location) const {
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
return uniform.name;
}
const String& GetActiveUniformName(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
return uniform.name;
}
// Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
static constexpr Uint kInvalidUniformOffset = ~0u;
Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; }
Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; }
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
Int GetAttributeLocation(const String& name) {
const auto it = std::find(m_attribs.begin(), m_attribs.end(), name);
return (it == m_attribs.end()) ? -1 : (Int)std::distance(m_attribs.begin(), it);
const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name);
return (it == Artifacts().attribs.end()) ? -1 : (Int)std::distance(Artifacts().attribs.begin(), it);
}
Uint32 GetActiveAttributeLocationMask() const {
Uint32 mask = 0;
const SizeT count = std::min<SizeT>(m_attribs.size(), 32);
const SizeT count = std::min<SizeT>(Artifacts().attribs.size(), 32);
for (SizeT index = 0; index < count; ++index) {
if (!m_attribs[index].empty()) {
if (!Artifacts().attribs[index].empty()) {
mask |= (1u << index);
}
}
return mask;
}
Uint32 GetActiveFragmentOutputLocationMask() const {
if (!m_program) {
if (!Artifacts().program) {
return 0;
}
Uint32 mask = 0;
const Int outputCount = m_program->getNumPipeOutputs();
const Int outputCount = Artifacts().program->getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) {
const Int location = static_cast<Int>(m_program->getPipeOutput(index).layoutLocation());
const Int location = static_cast<Int>(Artifacts().program->getPipeOutput(index).layoutLocation());
if (location >= 0 && location < 32) {
mask |= (1u << location);
}
@@ -319,38 +319,38 @@ namespace MobileGL::MG_State::GLState {
return mask;
}
Int GetActiveFragmentOutputCount() const {
return m_program ? m_program->getNumPipeOutputs() : 0;
return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0;
}
const String& GetActiveFragmentOutputName(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
return m_program->getPipeOutput(static_cast<Int>(index)).name;
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).name;
}
Int GetFragmentOutputLocation(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputLocation: index=%u out of range",
index);
return static_cast<Int>(m_program->getPipeOutput(static_cast<Int>(index)).layoutLocation());
return static_cast<Int>(Artifacts().program->getPipeOutput(static_cast<Int>(index)).layoutLocation());
}
GLint GetActiveFragmentOutputArraySize(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
return m_program->getPipeOutput(static_cast<Int>(index)).size;
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).size;
}
GLenum GetFragmentOutputType(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputType: index=%u out of range",
index);
return m_program->getPipeOutput(static_cast<Int>(index)).glDefineType;
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).glDefineType;
}
GLenum GetAttribType(Uint index) const { return m_attribTypes[index]; }
const String& GetAttribName(Uint index) const { return m_attribs[index]; }
GLenum GetActiveAttribType(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).glDefineType; }
GLint GetActiveAttribArraySize(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).size; }
GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).glDefineType; }
GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).size; }
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names;
// GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
// resource queries enumerate builtins).
@@ -362,11 +362,11 @@ namespace MobileGL::MG_State::GLState {
return name;
}
const String& GetActiveAttribName(Uint index) const {
return NormalizeBuiltinPipeInputName(m_program->getPipeInput(static_cast<Int>(index)).name);
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
}
void* MapUBO() { return m_globalUboScratch.data(); }
const void* GetUBOData() const { return m_globalUboScratch.data(); }
Uint GetUBOSize() const { return static_cast<Uint>(m_globalUboScratch.size()); }
void* MapUBO() { return Artifacts().globalUboScratch.data(); }
const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
// backends' "never uploaded" sentinel, so skip over it on wrap.
@@ -412,20 +412,20 @@ namespace MobileGL::MG_State::GLState {
}
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
if (location >= m_uniformSamplerOrImageUnitIndex.size() ||
m_uniformSamplerOrImageUnitIndex[location] == unit) {
if (location >= Artifacts().uniformSamplerOrImageUnitIndex.size() ||
Artifacts().uniformSamplerOrImageUnitIndex[location] == unit) {
return;
}
m_uniformSamplerOrImageUnitIndex[location] = unit;
Artifacts().uniformSamplerOrImageUnitIndex[location] = unit;
++m_backendStateVersion;
}
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
return m_uniformSamplerOrImageUnitIndex[location];
return Artifacts().uniformSamplerOrImageUnitIndex[location];
}
Bool GetDeleteStatus() const { return m_deleteStatus; }
Bool GetLinkStatus() const { return m_linkStatus; }
Bool GetLinkStatus() const { return Artifacts().linkStatus; }
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format
// (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
// ARB_get_program_binary requires of it.
@@ -439,26 +439,27 @@ namespace MobileGL::MG_State::GLState {
// glProgramBinary always fails here (there is no format it could accept) and the
// spec then requires the program's LINK_STATUS to read FALSE.
void MarkLinkFailedByProgramBinary() {
BumpLinkObservableVersions();
ResetLinkArtifacts();
m_infoLog = "No program binary format is supported.";
Artifacts().infoLog = "No program binary format is supported.";
}
Bool GetValidateStatus() const { return m_validateStatus; }
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); }
Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); }
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
// materializes for default-block uniforms is filtered out by DoReflection.
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(m_glBlockIndexToTProgram.size()); }
GLuint GetComputeLocalSize(Uint dim) const { return m_program->getLocalSize(static_cast<Int>(dim)); }
Int GetActiveAttributesMaxLength() const { return m_attribInNameMaxLength; }
Int GetActiveUniformBlocksMaxNameLength() const { return m_uniformBlockNameMaxLength; }
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast<Int>(dim)); }
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
Uint GetUniformBlockIndex(const char* name) const {
auto it = m_uniformBlockIndexByName.find(name);
if (it != m_uniformBlockIndexByName.end()) return it->second;
auto it = Artifacts().uniformBlockIndexByName.find(name);
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
// a bare "Block" query resolves to the first instance per GL semantics.
const String suffixedName = String(name) + "[0]";
it = m_uniformBlockIndexByName.find(suffixedName);
if (it != m_uniformBlockIndexByName.end()) return it->second;
it = Artifacts().uniformBlockIndexByName.find(suffixedName);
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
return 0xFFFFFFFFu; // GL_INVALID_INDEX
}
Bool IsActiveUniformBlock(Uint index) const {
@@ -471,11 +472,11 @@ namespace MobileGL::MG_State::GLState {
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
return (m_program->getUniformBlock(m_glBlockIndexToTProgram[index]).size + 15u) & ~15u;
return (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u;
}
const String& GetUniformBlockName(Uint index) const {
auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]);
auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
return ubo.name;
}
@@ -487,8 +488,8 @@ namespace MobileGL::MG_State::GLState {
if (name.empty() || name.back() != ']') return index;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos) return index;
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
if (it != m_uniformBlockIndexByName.end()) return it->second;
const auto it = Artifacts().uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
return index;
}
@@ -499,31 +500,31 @@ namespace MobileGL::MG_State::GLState {
Int GetUniformBlockActiveUniformCount(Uint index) const {
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
Int count = 0;
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) {
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
}
return count;
}
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
const auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]);
const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
return (ubo.stages & stageMask) != 0;
}
// Set by glUniformBlockBinding
void SetUniformBlockBinding(Uint index, Uint binding) {
if (index >= m_uniformBlockBinding.size() || m_uniformBlockBinding[index] == static_cast<Int>(binding)) {
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
return;
}
m_uniformBlockBinding[index] = static_cast<Int>(binding);
Artifacts().uniformBlockBinding[index] = static_cast<Int>(binding);
++m_backendStateVersion;
}
Uint GetUniformBlockBinding(Uint index) const { return m_uniformBlockBinding[index]; }
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return m_generatedSpirv; }
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return m_generatedSpirv; }
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
Int GetShaderIndexByStage(ShaderStage stage) const {
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
@@ -545,41 +546,134 @@ namespace MobileGL::MG_State::GLState {
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
Uint32 packedOffsetBytes = 0;
};
// ---- P1: everything a link PRODUCES, in one movable block ----
//
// The membership rule is mechanical, not editorial: this is exactly the field list
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
// as much link output). Nothing else belongs here.
//
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
// of thirty cross-thread field assignments. Until then this is a pure refactor.
//
// Access rule (invariant I5): the member below is private and reachable ONLY
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
// what makes "every read of link output joins the pending link" a property the
// compiler checks rather than a review item - a new reader cannot spell the field
// without going through the gate.
struct LinkArtifacts {
SharedPtr<glslang::TProgram> program;
Vector<Vector<unsigned>> generatedSpirv;
// Attributes (Vertex in)
Vector<String> attribs;
Vector<GLenum> attribTypes;
// FragData (Frag out): the per-link snapshot of the explicit request maps.
UnorderedMap<String, Uint> linkedFragDataLocation;
UnorderedMap<String, Uint> linkedFragDataIndex;
// GL-facing index spaces (see the translation helpers above): GL active-uniform
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
Vector<Int> glUniformIndexToTProgram;
Vector<Int> tProgramUniformIndexToGl;
Vector<Int> glBlockIndexToTProgram;
Vector<Int> tProgramBlockIndexToGl;
// Per-link merged snapshot of the attached shaders' lexically extracted
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
// them from reflection; the DoReflection assigner restores them from here).
UnorderedMap<String, Int> linkedExplicitUniformLocations;
UnorderedMap<String, Uint> uniformLocations;
// Ordered by location,
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
Vector<Int> uniformIndexInTProgram;
// ditto. Will be set at glUniform1i
Vector<Int> uniformSamplerOrImageUnitIndex;
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
// Ordered by uniform block index
// index is DIFFERENT from binding!!!
//
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
// `prog->getUniformBlock(i) == "BlockName"`
// These stuff are present for GL semantics, not for backend inspection
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
UnorderedMap<String, Uint> uniformBlockIndexByName;
Vector<Int> uniformBlockBinding;
// Need to be reflected after linking of SPIR-V binary
Vector<Uint> uniformOffsets;
Vector<Uint> uniformSizesInBytes;
Vector<Uint8> globalUboScratch;
Uint activeUniformCount = 0;
Uint maxUniformLocation = 0;
Int uniformNameMaxLength = 0;
Int attribInNameMaxLength = 0;
Int uniformBlockNameMaxLength = 0;
String infoLog;
Bool linkStatus = false;
// Transform feedback: the linked snapshot (the request lives outside, on the
// GL-thread-owned side).
Vector<XfbVarying> xfbVaryings;
Vector<Uint32> xfbStrides;
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
GLenum gsInputPrimitive = GL_NONE;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false;
Uint32 xfbPackedStride = 0;
};
// Blocks until a pending link (P1 stage 4 onwards) has published its artifacts.
// Public because a few call sites have to join without reading anything - see the
// explicit-join list in the P1 design. Today there is never a pending link, so this
// is a no-op; it is wired up when glLinkProgram starts enqueueing.
void JoinLink() const { EnsureLinkJoined(); }
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode;
}
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr;
}
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return Artifacts().xfbVaryings; }
// Stride of one captured vertex in the given capture buffer slot.
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
return bufferIndex < Artifacts().xfbStrides.size() ? Artifacts().xfbStrides[bufferIndex] : 0;
}
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
SizeT GetTransformFeedbackBufferCount() const { return Artifacts().xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return Artifacts().xfbVaryingNameMaxLength; }
// True when the capture layout uses gl_SkipComponents / gl_NextBuffer
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every
// captured varying into one record with no gaps. A backend that captures through
// such a driver has to capture into scratch storage and scatter the records into the
// application's buffers itself, using packedOffsetBytes as the source offset and
// (bufferIndex, offsetBytes, stride) as the destination.
Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; }
Bool NeedsScatteredTransformFeedbackCapture() const { return Artifacts().xfbNeedsScatteredCapture; }
// Bytes one gap-free captured record occupies.
Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; }
Uint32 GetTransformFeedbackPackedStride() const { return Artifacts().xfbPackedStride; }
// True when the capture stage is a triangle-strip geometry shader with a
// statically-known emit sequence: the Vulkan capture order then needs the GL
// odd-triangle vertex swap after EndTransformFeedback.
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
Bool HasGsTriangleStripCaptureFixup() const { return Artifacts().gsStripCaptureFixup; }
// Triangles per strip, in emission order, for ONE geometry invocation.
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
const Vector<Uint32>& GetGsStripTriangles() const { return Artifacts().gsStripTriangles; }
// GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES,
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
// program has no geometry stage. Draws must present a compatible primitive type.
GLenum GetGeometryInputType() const { return m_gsInputPrimitive; }
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
@@ -589,99 +683,82 @@ namespace MobileGL::MG_State::GLState {
Uint64 GetLifetimeId() const { return m_lifetimeId; }
private:
// ---- The one and only join gate for link output (P1 invariant I5) ----
// Blocks until a pending link has finished and its LinkArtifacts have been
// published into m_artifacts. Today no link is ever pending - glLinkProgram still
// runs the whole body inline - so this is an unconditional no-op, and the whole
// Artifacts() indirection compiles away. It exists NOW so that the ~120 readers of
// link output are already routed through it when stage 4 makes it block: the edit
// that turns links asynchronous then touches this function and nothing else.
//
// Defined inline (not in ProgramObject.cpp) on purpose: this is called from every
// Artifacts() read - ~1200 call sites project-wide - and the project never builds
// with LTO (MOBILEGL_ENABLE_LTO=OFF), so an out-of-line empty body would leave a
// real cross-TU call at every one of them instead of folding away. Stage 4's
// version, which actually blocks, moves the wait itself out-of-line behind a
// `m_pendingLink` check that stays inline here.
void EnsureLinkJoined() const {}
LinkArtifacts& Artifacts() {
EnsureLinkJoined();
return m_artifacts;
}
const LinkArtifacts& Artifacts() const {
EnsureLinkJoined();
return m_artifacts;
}
void ResetLinkArtifacts();
// GL-thread-only companion to ResetLinkArtifacts (see its definition).
void BumpLinkObservableVersions();
// Builds the GL-facing reflection surface from the linked TProgram. Returns false
// (with m_infoLog set and link artifacts reset) when reflection itself fails or an
// (with the artifacts' infoLog set and link artifacts reset) when reflection itself fails or an
// explicit-uniform-location conflict makes the link invalid.
Bool DoReflection();
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
// Resolves the requested transform feedback varyings against the linked
// vertex stage; fails the link (GL semantics) on unknown or duplicate
// names or exceeded capture limits.
Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
// The former GenerateBinary, split around DoReflection's data dependencies:
// SPIR-V must be generated BEFORE buildReflection touches m_program (its
// SPIR-V must be generated BEFORE buildReflection touches the linked TProgram (its
// live-variable analysis mutates the intermediates enough to change
// GlslangToSpv output), while the glUniform*-to-global-UBO routing tables are
// sized and keyed by reflection results (m_maxUniformLocation,
// m_uniformLocations) and so must run AFTER it.
// sized and keyed by reflection results (maxUniformLocation, uniformLocations)
// and so must run AFTER it.
void GenerateSpirv();
void BuildGlobalUboRouting();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing();
Bool ValidateFragmentOutputLocations();
static Uint64 AllocateLifetimeId();
// ---- GL-thread-owned state: never joins ----
// Most of this is never produced by a link at all. The three version counters
// (m_backendStateVersion / m_uboContentVersion / m_linkVersion) ARE
// link-observable, but they are bumped exclusively on the GL thread
// (BumpLinkObservableVersions in Link()'s prologue and glProgramBinary's
// failure path) - the link BODY, which stage 4 moves to a worker, never
// writes them.
const Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = 0;
// The attach lists are mutated only in Link()'s GL-thread prologue, which is why
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
Vector<SharedPtr<ShaderObject>> m_shaders;
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
SharedPtr<glslang::TProgram> m_program;
Vector<Vector<unsigned>> m_generatedSpirv;
// Attributes (Vertex in)
// Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation,
// glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer
// count stamped in by the entry point. A pending link snapshots these at enqueue.
UnorderedMap<String, Uint> m_explicitAttribLocations;
Vector<String> m_attribs;
Vector<GLenum> m_attribTypes;
// FragData (Frag out)
UnorderedMap<String, Uint> m_explicitFragDataLocation;
UnorderedMap<String, Uint> m_linkedFragDataLocation;
// Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted
// into the linked map at link time, like the location maps above.
UnorderedMap<String, Uint> m_explicitFragDataIndex;
UnorderedMap<String, Uint> m_linkedFragDataIndex;
Int m_maxFragmentOutputColorNumber = 8;
Vector<String> m_requestedXfbVaryings;
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
// Uniforms
// GL-facing index spaces (see the translation helpers above): GL active-uniform
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
Vector<Int> m_glUniformIndexToTProgram;
Vector<Int> m_tProgramUniformIndexToGl;
Vector<Int> m_glBlockIndexToTProgram;
Vector<Int> m_tProgramBlockIndexToGl;
// Per-link merged snapshot of the attached shaders' lexically extracted
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
// them from reflection; the DoReflection assigner restores them from here).
UnorderedMap<String, Int> m_linkedExplicitUniformLocations;
UnorderedMap<String, Uint> m_uniformLocations;
// Ordered by location,
// aka. m_uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
Vector<Int> m_uniformIndexInTProgram;
// ditto. Will be set at glUniform1i
Vector<Int> m_uniformSamplerOrImageUnitIndex;
UnorderedMap<String, Uint> m_explicitOpaqueUniformBindings;
// Ordered by uniform block index
// index is DIFFERENT from binding!!!
//
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
// `prog->getUniformBlock(i) == "BlockName"`
// These stuff are present for GL semantics, not for backend inspection
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
UnorderedMap<String, Uint> m_uniformBlockIndexByName;
Vector<Int> m_uniformBlockBinding;
// Need to be reflected after linking of SPIR-V binary
Vector<Uint> m_uniformOffsets;
Vector<Uint> m_uniformSizesInBytes;
Vector<Uint8> m_globalUboScratch;
Uint m_activeUniformCount = 0;
Uint m_maxUniformLocation = 0;
Int m_uniformNameMaxLength = 0;
Int m_attribInNameMaxLength = 0;
Int m_uniformBlockNameMaxLength = 0;
String m_infoLog;
Bool m_deleteStatus = false;
Bool m_linkStatus = false;
Bool m_binaryRetrievableHint = false;
Bool m_separable = false;
Bool m_validateStatus = true;
@@ -704,17 +781,9 @@ namespace MobileGL::MG_State::GLState {
Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0;
// Transform feedback: request (applies at next link) and linked snapshot.
Vector<String> m_requestedXfbVaryings;
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Vector<XfbVarying> m_xfbVaryings;
Vector<Uint32> m_xfbStrides;
Vector<Uint32> m_gsStripTriangles;
Bool m_gsStripCaptureFixup = false;
GLenum m_gsInputPrimitive = GL_NONE;
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int m_xfbVaryingNameMaxLength = 0;
Bool m_xfbNeedsScatteredCapture = false;
Uint32 m_xfbPackedStride = 0;
// ---- Link OUTPUT ----
// Written by the link and by the post-link setters GL allows (glUniform1i's sampler
// unit, glUniformBlockBinding). Reachable only through Artifacts(); see LinkArtifacts.
LinkArtifacts m_artifacts;
};
} // namespace MobileGL::MG_State::GLState
@@ -79,7 +79,7 @@ namespace MobileGL::MG_State::GLState {
Uint shaderId = 0;
m_programShaderNameGenerator.Generate(1, &shaderId);
EnsureIndexAvail(shaderId, m_shaderObjects);
auto shaderObject = MakeShared<ShaderObject>(stage, shaderId, &m_shaderPreprocessCache);
auto shaderObject = MakeShared<ShaderObject>(stage, shaderId, m_shaderPreprocessCache);
if (shaderObject == nullptr) return 0;
m_shaderObjects[shaderId] = shaderObject;
return shaderId;
@@ -37,7 +37,7 @@ namespace MobileGL::MG_State::GLState {
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
// directly - shader objects reach it through the pointer they are handed at
// CreateShader().
ShaderPreprocessCache& GetShaderPreprocessCache() { return m_shaderPreprocessCache; }
ShaderPreprocessCache& GetShaderPreprocessCache() { return *m_shaderPreprocessCache; }
private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
@@ -64,10 +64,11 @@ namespace MobileGL::MG_State::GLState {
// object kinds keeps the names disjoint; the object tables stay separate.
IndexGenerator<Uint> m_programShaderNameGenerator;
// P0b layer 2: every shader object created here is handed a pointer to this cache.
// Declared FIRST on purpose - members are destroyed in reverse declaration order,
// so the cache outlives every shader object holding a pointer to it.
ShaderPreprocessCache m_shaderPreprocessCache;
// P0b layer 2: every shader object created here is handed shared ownership of this
// cache, so its lifetime no longer depends on member destruction order (P1: an
// in-flight compile job may outlive the context). The FIRST-member declaration is
// kept anyway - it costs nothing and documents the intent.
SharedPtr<ShaderPreprocessCache> m_shaderPreprocessCache = MakeShared<ShaderPreprocessCache>();
Vector<SharedPtr<ProgramObject>> m_programObjects;
Vector<SharedPtr<ShaderObject>> m_shaderObjects;
@@ -12,8 +12,8 @@
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
#include <MG_Backend/BackendObjects.h>
#include <charconv>
@@ -115,41 +115,22 @@ namespace {
return localSize;
}
static MobileGL::Uint GetComputeWorkGroupSizeLimit(MobileGL::Uint index) {
constexpr MobileGL::Uint kFrontendMinComputeWorkGroupSizes[] = {1024, 1024, 64};
MobileGL::Int backendValue = 0;
if (MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
&backendValue);
}
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
return std::max(static_cast<MobileGL::Uint>(std::max(backendValue, 0)),
kFrontendMinComputeWorkGroupSizes[index]);
}
static unsigned long long GetComputeWorkGroupInvocationLimit() {
constexpr unsigned long long kFrontendMaxComputeWorkGroupInvocations = 1024;
if (!MobileGL::MG_Backend::pActiveBackendObject) return kFrontendMaxComputeWorkGroupInvocations;
return std::max(static_cast<unsigned long long>(std::max(
MobileGL::MG_Backend::pActiveBackendObject->GetDynamicParameters()
.MaxComputeWorkGroupInvocations,
0)),
kFrontendMaxComputeWorkGroupInvocations);
}
static std::optional<MobileGL::String> ValidateComputeLocalSizeLimits(const MobileGL::String& source) {
// The device limits come from the CompileEnv snapshot, never from a live driver query.
// GL_MAX_COMPUTE_WORK_GROUP_SIZE is a real GLES call on the DirectGLES backend: issued
// off the context thread it would silently no-op and turn a legal local_size_z into
// COMPILE_STATUS=FALSE. CaptureCompileEnv() issues it once, on the GL thread.
static std::optional<MobileGL::String> ValidateComputeLocalSizeLimits(
const MobileGL::String& source, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const ComputeLocalSize localSize = ParseComputeLocalSize(source);
if (!localSize.declared) return std::nullopt;
if (localSize.x > GetComputeWorkGroupSizeLimit(0) || localSize.y > GetComputeWorkGroupSizeLimit(1) ||
localSize.z > GetComputeWorkGroupSizeLimit(2)) {
if (localSize.x > env.maxComputeWorkGroupSize[0] || localSize.y > env.maxComputeWorkGroupSize[1] ||
localSize.z > env.maxComputeWorkGroupSize[2]) {
return "Compute shader local_size exceeds GL_MAX_COMPUTE_WORK_GROUP_SIZE.";
}
const unsigned long long invocations = static_cast<unsigned long long>(localSize.x) * localSize.y * localSize.z;
if (invocations > GetComputeWorkGroupInvocationLimit()) {
if (invocations > env.maxComputeWorkGroupInvocations) {
return "Compute shader local_size product exceeds GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS.";
}
@@ -162,23 +143,23 @@ namespace {
// nothing else - the glslang parse stays per-object because its TShader is
// consume-once. Deliberately free of any per-object state so the memo is sound.
//
// Caveat, documented rather than defended against: the compute local-size verdict also
// reads the active backend's GL_MAX_COMPUTE_WORK_GROUP_* limits. Those are fixed for
// the lifetime of a context, and the cache is per-context, so the memo cannot outlive
// the limits it was computed against.
// The former caveat is gone: the compute local-size verdict reads `env` rather than the
// live backend, and env.fingerprint is part of the P0b cache key, so a memo can never be
// returned against limits other than the ones it was computed against.
static MobileGL::MG_State::GLState::ShaderPreprocessResult RunSourceOnlyPipeline(
const MobileGL::ShaderStage stage, const MobileGL::String& source) {
const MobileGL::ShaderStage stage, const MobileGL::String& source,
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
using namespace MobileGL;
using namespace MobileGL::MG_Util::ShaderTranspiler;
using MobileGL::MG_State::GLState::ShaderPreprocessOutcome;
MobileGL::MG_State::GLState::ShaderPreprocessResult result;
result.preprocessedSource = source;
PreprocessShaderSource(stage, result.preprocessedSource);
PreprocessShaderSource(stage, result.preprocessedSource, env);
if (stage == ShaderStage::Compute) {
if (const std::optional<String> localSizeError =
ValidateComputeLocalSizeLimits(result.preprocessedSource)) {
ValidateComputeLocalSizeLimits(result.preprocessedSource, env)) {
result.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
result.infoLog = *localSizeError;
return result;
@@ -240,14 +221,14 @@ namespace MobileGL::MG_State::GLState {
m_compiledSourceLength = m_source.length();
}
// EnsureCompileJoined() is defined inline in ShaderObject.h (see the comment there for
// why: no LTO, and it is called from every Compiled() read).
void ShaderObject::InvalidateCompiledState() {
m_shader.reset();
m_preprocessedSource.clear();
m_explicitUniformLocations.clear();
m_explicitOpaqueBindings.clear();
m_shaderConsumedByLink = false;
m_compileStatus = false;
m_infoLog.clear();
// The compile artifacts are exactly what one Compile() writes, so discarding them
// wholesale IS the invalidation. (Kept as an explicit reset rather than a
// default-construct so the intent survives a future field addition.)
Compiled() = CompileArtifacts{};
m_hasCompiledState = false;
m_compiledSourceHash = 0;
m_compiledSourceLength = 0;
@@ -260,8 +241,8 @@ namespace MobileGL::MG_State::GLState {
// the exact source it still holds, so a recompile is a no-op. This covers the
// failure case too - the info log stays queryable because nothing is cleared.
//
// m_shaderConsumedByLink interaction: if the stored TShader already fed a link,
// the no-op leaves m_preprocessedSource and both side-channel maps intact, which
// shaderConsumedByLink interaction: if the stored TShader already fed a link,
// the no-op leaves preprocessedSource and both side-channel maps intact, which
// is precisely what TakeShaderForLink's on-demand re-parse needs. A real recompile
// would have handed the next link a fresh parse; the no-op hands it a fresh
// re-parse of the identical source instead. Same result, one parse either way.
@@ -271,59 +252,72 @@ namespace MobileGL::MG_State::GLState {
const Uint64 sourceHash = ShaderPreprocessCache::HashSource(m_source);
// The compile-environment snapshot, taken here on the GL thread. Everything below
// reads the device through it and never through pActiveBackendObject, which is what
// makes the whole body movable onto a worker in stage 3.
CompileArtifacts& compiled = Compiled();
compiled.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
const MG_Util::ShaderTranspiler::CompileEnv& env = *compiled.env;
// P0b layer 2: another shader object in this context may already have run the
// source-only half over byte-identical text.
const ShaderPreprocessResult* cached =
m_preprocessCache != nullptr ? m_preprocessCache->Find(m_stage, sourceHash, m_source) : nullptr;
ShaderPreprocessResult fresh;
if (cached == nullptr) fresh = RunSourceOnlyPipeline(m_stage, m_source);
const ShaderPreprocessResult& shared = cached != nullptr ? *cached : fresh;
const Bool shouldPopulateCache = cached == nullptr && m_preprocessCache != nullptr;
// source-only half over byte-identical text under the same environment.
ShaderPreprocessResultPtr cached =
m_preprocessCache ? m_preprocessCache->Find(m_stage, sourceHash, m_source, env.fingerprint) : nullptr;
SharedPtr<ShaderPreprocessResult> fresh;
if (!cached) fresh = MakeShared<ShaderPreprocessResult>(RunSourceOnlyPipeline(m_stage, m_source, env));
const ShaderPreprocessResult& shared = cached ? *cached : *fresh;
const Bool shouldPopulateCache = !cached && m_preprocessCache != nullptr;
if (!shared.Preprocessed()) {
// Rejected lexically, or a glslang failure this context has already seen for
// this exact source (ParseFailed) - either way the parse can be skipped.
m_infoLog = shared.infoLog;
if (shouldPopulateCache) m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh));
compiled.infoLog = shared.infoLog;
if (shouldPopulateCache) {
m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh));
}
RememberCompiledSource(sourceHash);
return;
}
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
.sourceStr = shared.preprocessedSource,
.flags = 0};
.flags = 0,
.env = &env};
auto result = ShaderCompiler::CompileShader(attrib);
if (result) {
m_compileStatus = true;
m_shader = result.value();
compiled.compileStatus = true;
compiled.shader = result.value();
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and
// `fresh` is about to be handed to the cache.
m_preprocessedSource = shared.preprocessedSource;
m_explicitUniformLocations = shared.explicitUniformLocations;
m_explicitOpaqueBindings = shared.explicitOpaqueBindings;
m_infoLog.clear();
if (shouldPopulateCache) m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh));
} else {
m_infoLog = result.error().log;
MGLOG_D("ShaderObject::Compile: Shader %d compilation failed.\nSource:\n%s\nInfoLog:\n%s\nSetting "
"m_compileStatus = false as a result.",
m_externalIndex, shared.preprocessedSource.c_str(), m_infoLog.c_str());
compiled.preprocessedSource = shared.preprocessedSource;
compiled.explicitUniformLocations = shared.explicitUniformLocations;
compiled.explicitOpaqueBindings = shared.explicitOpaqueBindings;
compiled.infoLog.clear();
if (shouldPopulateCache) {
fresh.outcome = ShaderPreprocessOutcome::ParseFailed;
fresh.infoLog = m_infoLog;
fresh.explicitUniformLocations.clear();
fresh.explicitOpaqueBindings.clear();
m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh));
m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh));
}
} else {
compiled.infoLog = result.error().log;
MGLOG_D("ShaderObject::Compile: Shader %d compilation failed.\nSource:\n%s\nInfoLog:\n%s\nSetting "
"compileStatus = false as a result.",
m_externalIndex, shared.preprocessedSource.c_str(), compiled.infoLog.c_str());
if (shouldPopulateCache) {
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
fresh->infoLog = compiled.infoLog;
fresh->explicitUniformLocations.clear();
fresh->explicitOpaqueBindings.clear();
m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh));
}
}
RememberCompiledSource(sourceHash);
}
SharedPtr<glslang::TShader> ShaderObject::TakeShaderForLink(String& outReparseLog) {
if (m_shader && !m_shaderConsumedByLink) {
m_shaderConsumedByLink = true;
return m_shader;
CompileArtifacts& compiled = Compiled();
if (compiled.shader && !compiled.shaderConsumedByLink) {
compiled.shaderConsumedByLink = true;
return compiled.shader;
}
// The stored parse already fed a link, whose mapIO mutated its intermediate.
@@ -332,8 +326,11 @@ namespace MobileGL::MG_State::GLState {
// here on EVERY link rather than only on reuse.
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
.sourceStr = m_preprocessedSource,
.flags = 0};
.sourceStr = compiled.preprocessedSource,
.flags = 0,
// Re-parse against the SAME environment the original parse used,
// not against whatever the backend reports now.
.env = compiled.env.get()};
auto result = ShaderCompiler::CompileShader(attrib);
if (!result) {
// Should be unreachable: the same source parsed successfully at Compile().
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL {
enum class ShaderStage {
@@ -31,9 +32,12 @@ namespace MobileGL {
// `preprocessCache` is the owning context's cross-object memo (P0b layer 2);
// null is fully supported and simply means "no sharing" - that is what the
// context-less internal shader objects (the default FS, the blit pipeline) use.
// Shared ownership rather than a raw pointer: once compiles run on a worker the
// job outlives neither the object nor the context deterministically, and the
// cache has to stay alive for whoever is still reading it.
ShaderObject(const ShaderStage stage, Uint externalIndex,
ShaderPreprocessCache* preprocessCache = nullptr)
: m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(preprocessCache) {}
SharedPtr<ShaderPreprocessCache> preprocessCache = nullptr)
: m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(Move(preprocessCache)) {}
void SetShaderSource(const String& source);
void SetShaderSource(String&& source);
void Compile();
@@ -51,29 +55,83 @@ namespace MobileGL {
Uint GetExternalIndex() const { return m_externalIndex; }
ShaderStage GetShaderStage() const { return m_stage; }
const String& GetShaderSource() const { return m_source; }
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return m_shader; }
const String& GetInfoLog() const { return m_infoLog; }
const UnorderedMap<String, Uint>& GetUniformLocations() const { return m_uniforms; }
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return Compiled().infoLog; }
const UnorderedMap<String, Uint>& GetUniformLocations() const { return Compiled().uniforms; }
// Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, captured lexically at Compile() because the relaxed parse drops
// them from reflection (see ExtractExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return m_explicitUniformLocations;
return Compiled().explicitUniformLocations;
}
// Explicit layout(binding = N) on sampler/image uniforms - their initial
// texture/image units - captured lexically for the same reason (see
// ExtractExplicitOpaqueBindings).
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const { return m_explicitOpaqueBindings; }
Bool GetCompileStatus() const { return m_compileStatus; }
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
return Compiled().explicitOpaqueBindings;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; }
// Blocks until a pending compile (P1 stage 3 onwards) has published its
// artifacts. Public for the few sites that must join without reading anything.
// A no-op today - nothing is ever pending.
void JoinCompile() const { EnsureCompileJoined(); }
// True while this object holds the outcome (success OR failure) of a previous
// Compile() of exactly the source it currently holds - i.e. while the P0b
// layer-1 memo is armed and a glCompileShader would be a no-op. Diagnostics
// and tests only; nothing in the GL frontend branches on it.
//
// Deliberately does NOT join: the memo bookkeeping below is GL-thread-owned and
// says nothing about whether a worker has finished, which is exactly the
// property GL_COMPLETION_STATUS_KHR needs when stage 3 lands.
Bool HasMemoizedCompile() const { return m_hasCompiledState; }
private:
// ---- P1: everything a compile PRODUCES, in one block ----
//
// Same rule as ProgramObject::LinkArtifacts: this is exactly what
// InvalidateCompiledState() clears, i.e. exactly what one run of Compile()
// writes. Stage 3 lifts this struct wholesale into ShaderCompileTask, where a
// worker fills it in and the GL thread reads it through the same gate.
struct CompileArtifacts {
// The CompileEnv snapshot this compile ran against. Held so the
// consume-once re-parse in TakeShaderForLink() reproduces the original
// parse exactly, instead of re-reading whatever the backend says now.
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
SharedPtr<glslang::TShader> shader;
// The source Compile() actually parsed (after PreprocessShaderSource), kept
// for TakeShaderForLink's re-parse so a later link never depends on the
// preprocessor being deterministic across backend-state changes.
String preprocessedSource;
UnorderedMap<String, Uint> uniforms;
UnorderedMap<String, Int> explicitUniformLocations;
UnorderedMap<String, Uint> explicitOpaqueBindings;
Bool shaderConsumedByLink = false;
String infoLog;
Bool compileStatus = false;
};
// ---- The one and only join gate for compile output (P1 invariant I5) ----
// Blocks until a pending compile has published into m_compiled. Today nothing
// is ever pending - glCompileShader still runs the whole body inline - so this
// is an unconditional no-op. It exists NOW so that every reader of compile
// output is already routed through it when stage 3 makes it block.
//
// Defined inline (not in ShaderObject.cpp): called from every Compiled() read,
// and the project never builds with LTO, so an out-of-line empty body would be
// a real cross-TU call at each of those call sites instead of folding away.
void EnsureCompileJoined() const {}
CompileArtifacts& Compiled() {
EnsureCompileJoined();
return m_compiled;
}
const CompileArtifacts& Compiled() const {
EnsureCompileJoined();
return m_compiled;
}
void InvalidateCompiledState();
// ---- P0b layer 1: per-object no-op recompile ----
// True iff `candidate` is byte-identical to the source that produced the
@@ -84,31 +142,27 @@ namespace MobileGL {
// Arms the layer-1 memo for the source that Compile() just processed.
void RememberCompiledSource(Uint64 sourceHash);
// ---- GL-thread-owned state: never produced by a compile, so it never joins ----
const Uint m_externalIndex = 0;
const ShaderStage m_stage;
// glShaderSource text. A worker only ever reads the snapshot handed to it, so
// GL_SHADER_SOURCE_LENGTH and glGetShaderSource never join.
String m_source;
// The source Compile() actually parsed (after PreprocessShaderSource), kept
// for TakeShaderForLink's re-parse so a later link never depends on the
// preprocessor being deterministic across backend-state changes.
String m_preprocessedSource;
SharedPtr<glslang::TShader> m_shader;
UnorderedMap<String, Uint> m_uniforms;
UnorderedMap<String, Int> m_explicitUniformLocations;
UnorderedMap<String, Uint> m_explicitOpaqueBindings;
Bool m_shaderConsumedByLink = false;
// P0b layer 2: the owning context's cross-object memo, or null. Not owned.
ShaderPreprocessCache* const m_preprocessCache = nullptr;
// P0b layer 2: the owning context's cross-object memo, or null.
const SharedPtr<ShaderPreprocessCache> m_preprocessCache;
// P0b layer 1. m_hasCompiledState is the invariant "m_source is byte-identical
// to the source that produced m_compileStatus/m_infoLog/m_shader"; it is armed
// at the end of every Compile() and disarmed by InvalidateCompiledState().
// to the source that produced the compile artifacts"; it is armed at the end of
// every Compile() and disarmed by InvalidateCompiledState(). Stage 3 replaces
// all three with a pointer compare against the in-flight job's source snapshot.
Bool m_hasCompiledState = false;
Uint64 m_compiledSourceHash = 0;
SizeT m_compiledSourceLength = 0;
String m_infoLog;
Bool m_deleteStatus = false;
Bool m_compileStatus = false;
// ---- Compile OUTPUT ---- reachable only through Compiled().
CompileArtifacts m_compiled;
};
} // namespace MG_State::GLState
} // namespace MobileGL
@@ -9,9 +9,14 @@
#include "ShaderPreprocessCache.h"
namespace MobileGL::MG_State::GLState {
const ShaderPreprocessResult* ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash,
const String& source) const {
const Key key{.stage = stage, .sourceHash = sourceHash, .sourceLength = source.length()};
ShaderPreprocessResultPtr ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash,
const String& source, const Uint64 envFingerprint) const {
const Key key{.stage = stage,
.sourceHash = sourceHash,
.sourceLength = source.length(),
.envFingerprint = envFingerprint};
const std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_index.find(key);
if (it == m_index.end()) return nullptr;
@@ -20,52 +25,62 @@ namespace MobileGL::MG_State::GLState {
const Entry& entry = *it->second;
if (entry.originalSource != source) return nullptr;
return &entry.result;
// A copy of the SharedPtr, taken under the lock: the payload now outlives any
// eviction the caller races with.
return entry.result;
}
void ShaderPreprocessCache::Insert(const ShaderStage stage, const Uint64 sourceHash, const String& source,
ShaderPreprocessResult result) {
const SizeT entryBytes = EntryBytes(source, result);
const Uint64 envFingerprint, ShaderPreprocessResultPtr result) {
if (!result) return;
const SizeT entryBytes = EntryBytes(source, *result);
// A single source bigger than the whole budget would evict every other entry and
// then itself; refuse it instead of thrashing the cache empty.
if (entryBytes > kMaxStoredSourceBytes) return;
const Key key{.stage = stage, .sourceHash = sourceHash, .sourceLength = source.length()};
const Key key{.stage = stage,
.sourceHash = sourceHash,
.sourceLength = source.length(),
.envFingerprint = envFingerprint};
const std::lock_guard<std::mutex> lock(m_mutex);
if (const auto existing = m_index.find(key); existing != m_index.end()) {
// Either a re-insert of the same source (harmless) or a genuine hash collision
// with a different source. Both are resolved by letting the newcomer win: one
// entry per key keeps the index a plain map, and a collision is astronomically
// rare enough that the loser simply misses.
EraseEntry(existing->second);
EraseEntryLocked(existing->second);
}
m_entries.push_back(Entry{.key = key, .originalSource = source, .result = Move(result)});
m_index[key] = std::prev(m_entries.end());
m_storedSourceBytes += entryBytes;
EvictUntilWithinBudget();
EvictUntilWithinBudgetLocked();
}
void ShaderPreprocessCache::Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_entries.clear();
m_index.clear();
m_storedSourceBytes = 0;
}
void ShaderPreprocessCache::EraseEntry(const EntryList::iterator it) {
const SizeT bytes = EntryBytes(it->originalSource, it->result);
void ShaderPreprocessCache::EraseEntryLocked(const EntryList::iterator it) {
const SizeT bytes = EntryBytes(it->originalSource, *it->result);
m_storedSourceBytes = bytes > m_storedSourceBytes ? 0 : m_storedSourceBytes - bytes;
m_index.erase(it->key);
m_entries.erase(it);
}
void ShaderPreprocessCache::EvictUntilWithinBudget() {
void ShaderPreprocessCache::EvictUntilWithinBudgetLocked() {
// FIFO: the oldest insertion goes first. Insert() already refuses entries larger
// than the byte budget, so this loop always terminates with at least the entry
// that was just added still resident.
while (!m_entries.empty() &&
(m_entries.size() > kMaxEntries || m_storedSourceBytes > kMaxStoredSourceBytes)) {
EraseEntry(m_entries.begin());
EraseEntryLocked(m_entries.begin());
}
}
} // namespace MobileGL::MG_State::GLState
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include <list>
#include <mutex>
#include <MG_State/GLState/ProgramState/ShaderObject.h>
namespace MobileGL::MG_State::GLState {
@@ -42,6 +43,11 @@ namespace MobileGL::MG_State::GLState {
Bool Preprocessed() const { return outcome == ShaderPreprocessOutcome::Preprocessed; }
};
// Cache hits hand out shared ownership, not a raw pointer into the entry list. That is
// what makes the cache safe once compiles run concurrently: a reader keeps its payload
// alive across any eviction, and a 107 KB preprocessedSource is never copied on a hit.
using ShaderPreprocessResultPtr = SharedPtr<const ShaderPreprocessResult>;
// P0b layer 2: a per-context, bounded memo of the source-only half of shader
// compilation, keyed by (stage, xxhash64(source), source length).
//
@@ -71,14 +77,22 @@ namespace MobileGL::MG_State::GLState {
static constexpr SizeT kMaxEntries = 128;
static constexpr SizeT kMaxStoredSourceBytes = 8u * 1024u * 1024u;
// Returns the memoized result for this exact source, or null on a miss. The
// returned pointer stays valid until the next Insert()/Clear() on this cache.
const ShaderPreprocessResult* Find(ShaderStage stage, Uint64 sourceHash, const String& source) const;
// Returns the memoized result for this exact source under this exact compile
// environment, or null on a miss. The returned SharedPtr owns its payload, so it
// stays valid for as long as the caller holds it - across Insert(), Clear(), and
// across the destruction of the cache itself.
//
// envFingerprint joins the key because the source-only pipeline's compute
// local-size verdict is computed against CompileEnv's device limits: a memo must
// never outlive the environment it was computed against (memo-hazard rule).
ShaderPreprocessResultPtr Find(ShaderStage stage, Uint64 sourceHash, const String& source,
Uint64 envFingerprint) const;
// Memoizes `result` for this source. A source whose own storage cost already
// exceeds the byte budget is simply not cached (caching it would evict everything
// else and then itself).
void Insert(ShaderStage stage, Uint64 sourceHash, const String& source, ShaderPreprocessResult result);
void Insert(ShaderStage stage, Uint64 sourceHash, const String& source, Uint64 envFingerprint,
ShaderPreprocessResultPtr result);
void Clear();
@@ -86,17 +100,25 @@ namespace MobileGL::MG_State::GLState {
return static_cast<Uint64>(XXH64(source.data(), source.length(), 0));
}
SizeT GetEntryCount() const { return m_entries.size(); }
SizeT GetStoredSourceBytes() const { return m_storedSourceBytes; }
SizeT GetEntryCount() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_entries.size();
}
SizeT GetStoredSourceBytes() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_storedSourceBytes;
}
private:
struct Key {
ShaderStage stage = ShaderStage::Unknown;
Uint64 sourceHash = 0;
SizeT sourceLength = 0;
Uint64 envFingerprint = 0;
Bool operator==(const Key& other) const {
return stage == other.stage && sourceHash == other.sourceHash && sourceLength == other.sourceLength;
return stage == other.stage && sourceHash == other.sourceHash &&
sourceLength == other.sourceLength && envFingerprint == other.envFingerprint;
}
};
@@ -107,6 +129,7 @@ namespace MobileGL::MG_State::GLState {
Uint64 mixed = key.sourceHash;
mixed ^= static_cast<Uint64>(key.sourceLength) + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
mixed ^= static_cast<Uint64>(static_cast<Int>(key.stage)) * 0xff51afd7ed558ccdull;
mixed ^= key.envFingerprint + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
return static_cast<SizeT>(mixed);
}
};
@@ -116,7 +139,7 @@ namespace MobileGL::MG_State::GLState {
// The full original (pre-preprocess) source, kept so a hit can be confirmed by
// comparison instead of trusting the hash.
String originalSource;
ShaderPreprocessResult result;
ShaderPreprocessResultPtr result;
};
using EntryList = std::list<Entry>;
@@ -125,14 +148,16 @@ namespace MobileGL::MG_State::GLState {
return source.length() + result.preprocessedSource.length();
}
void EraseEntry(EntryList::iterator it);
void EvictUntilWithinBudget();
void EvictUntilWithinBudgetLocked();
// P1: needs a mutex when compiles go async. Everything here is reached from
// glCompileShader on the single GL thread that owns the context, so today the
// cache is deliberately lock-free; the moment shader compilation moves onto a
// worker pool, Find/Insert/Clear all become critical sections (and Find's returned
// pointer stops being safe to hold across an Insert).
void EraseEntryLocked(EntryList::iterator it);
// P1: every public entry point takes this. The lock alone would NOT have been
// enough - the old Find() handed back a raw pointer into an entry that a
// concurrent Insert()'s FIFO eviction could erase while the caller was still
// reading it. Shared ownership of the payload is what closes that hole; the mutex
// only protects the containers below.
mutable std::mutex m_mutex;
EntryList m_entries; // front = oldest (FIFO victim)
UnorderedMap<Key, EntryList::iterator, KeyHasher> m_index;
SizeT m_storedSourceBytes = 0;
+154 -39
View File
@@ -21,6 +21,7 @@
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <spirv-tools/libspirv.hpp>
#include <spirv-tools/optimizer.hpp>
@@ -2378,6 +2379,78 @@ void main() {
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// P1: CompileEnv - the compile pipeline's snapshot of everything outside
// (stage, source). These pin the two properties the rest of P1 rides on: the
// compute limits really are carried in the snapshot (an off-thread
// GL_MAX_COMPUTE_WORK_GROUP_SIZE query would silently return 0 and reject a
// legal local_size), and the fingerprint really does move when they do.
// ---------------------------------------------------------------------------
TEST_F(ProgramUtilTest, CompileEnvCarriesComputeLimitsAndFrontendMinima) {
using MobileGL::MG_Util::ShaderTranspiler::CaptureCompileEnv;
const auto env = CaptureCompileEnv();
ASSERT_NE(env, nullptr);
// With no backend the snapshot is the frontend minimum, never zero - the value an
// off-thread GetIntegeri_v would have left behind.
EXPECT_GE(env->maxComputeWorkGroupSize[0], 1024u);
EXPECT_GE(env->maxComputeWorkGroupSize[1], 1024u);
EXPECT_GE(env->maxComputeWorkGroupSize[2], 64u);
EXPECT_GE(env->maxComputeWorkGroupInvocations, 1024u);
EXPECT_NE(env->fingerprint, 0u);
}
TEST_F(ProgramUtilTest, CompileEnvFingerprintTracksEveryInput) {
using MobileGL::MG_Util::ShaderTranspiler::CompileEnv;
using MobileGL::MG_Util::ShaderTranspiler::ComputeCompileEnvFingerprint;
CompileEnv base;
const Uint64 baseline = ComputeCompileEnvFingerprint(base);
EXPECT_EQ(ComputeCompileEnvFingerprint(base), baseline) << "fingerprint must be deterministic";
// A device that allows a bigger workgroup than the frontend minimum is a DIFFERENT
// compile environment: a memo taken under the smaller limit must not be reusable.
CompileEnv biggerZ = base;
biggerZ.maxComputeWorkGroupSize[2] = 256;
EXPECT_NE(ComputeCompileEnvFingerprint(biggerZ), baseline);
CompileEnv moreInvocations = base;
moreInvocations.maxComputeWorkGroupInvocations = 2048;
EXPECT_NE(ComputeCompileEnvFingerprint(moreInvocations), baseline);
CompileEnv otherBackend = base;
otherBackend.backend = MobileGL::BackendType::DirectVulkan;
EXPECT_NE(ComputeCompileEnvFingerprint(otherBackend), baseline);
CompileEnv otherLimits = base;
otherLimits.params.MaxVertexAttribs = 31;
EXPECT_NE(ComputeCompileEnvFingerprint(otherLimits), baseline);
CompileEnv otherExtensions = base;
otherExtensions.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64);
EXPECT_NE(ComputeCompileEnvFingerprint(otherExtensions), baseline);
CompileEnv otherQuirk = base;
otherQuirk.subgroupPrefixScanQuirk = MobileGL::MG_Config::QuirkOverride::ForceOn;
EXPECT_NE(ComputeCompileEnvFingerprint(otherQuirk), baseline);
}
// The no-backend fallback must stay exactly what the pipeline used to do inline:
// everything counts as advertised, because there is nothing to gate against.
TEST_F(ProgramUtilTest, CompileEnvWithoutBackendAdvertisesEverything) {
using MobileGL::MG_Util::ShaderTranspiler::CompileEnv;
CompileEnv env;
EXPECT_FALSE(env.HasBackend());
EXPECT_TRUE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64));
env.backend = MobileGL::BackendType::DirectGLES;
EXPECT_TRUE(env.HasBackend());
EXPECT_FALSE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64));
env.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64);
EXPECT_TRUE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64));
}
// P0b layer 2: ShaderPreprocessCache, tested directly. The GL-level behaviour it
// enables is covered end to end in ProgramTest; these pin the container itself,
// where the interesting cases (hash collisions, both eviction budgets) are hard
@@ -2387,13 +2460,19 @@ namespace {
using MobileGL::MG_State::GLState::ShaderPreprocessCache;
using MobileGL::MG_State::GLState::ShaderPreprocessOutcome;
using MobileGL::MG_State::GLState::ShaderPreprocessResult;
using MobileGL::MG_State::GLState::ShaderPreprocessResultPtr;
ShaderPreprocessResult MakeResult(const String& preprocessed) {
ShaderPreprocessResult result;
result.outcome = ShaderPreprocessOutcome::Preprocessed;
result.preprocessedSource = preprocessed;
result.explicitUniformLocations["uMarker"] = 7;
result.explicitOpaqueBindings["sMarker"] = 3;
// The env fingerprint every test below keys against, unless it is specifically
// exercising the fingerprint itself.
constexpr MobileGL::Uint64 kEnvA = 0x1111'2222'3333'4444ull;
constexpr MobileGL::Uint64 kEnvB = 0x5555'6666'7777'8888ull;
ShaderPreprocessResultPtr MakeResult(const String& preprocessed) {
auto result = MakeShared<ShaderPreprocessResult>();
result->outcome = ShaderPreprocessOutcome::Preprocessed;
result->preprocessedSource = preprocessed;
result->explicitUniformLocations["uMarker"] = 7;
result->explicitOpaqueBindings["sMarker"] = 3;
return result;
}
} // namespace
@@ -2403,10 +2482,10 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) {
const String source = "// a shader\nvoid main() {}\n";
const Uint64 hash = ShaderPreprocessCache::HashSource(source);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA), nullptr);
cache.Insert(ShaderStage::Vertex, hash, source, MakeResult("vertex-preprocessed"));
const ShaderPreprocessResult* hit = cache.Find(ShaderStage::Vertex, hash, source);
cache.Insert(ShaderStage::Vertex, hash, source, kEnvA, MakeResult("vertex-preprocessed"));
const ShaderPreprocessResultPtr hit = cache.Find(ShaderStage::Vertex, hash, source, kEnvA);
ASSERT_NE(hit, nullptr);
EXPECT_TRUE(hit->Preprocessed());
EXPECT_EQ(hit->preprocessedSource, "vertex-preprocessed");
@@ -2419,12 +2498,12 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) {
// Byte-identical source, different stage: a different key, so still a miss. Two
// stages sharing one entry would hand a fragment shader a vertex preprocess.
EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source), nullptr);
cache.Insert(ShaderStage::Fragment, hash, source, MakeResult("fragment-preprocessed"));
const ShaderPreprocessResult* fragmentHit = cache.Find(ShaderStage::Fragment, hash, source);
EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source, kEnvA), nullptr);
cache.Insert(ShaderStage::Fragment, hash, source, kEnvA, MakeResult("fragment-preprocessed"));
const ShaderPreprocessResultPtr fragmentHit = cache.Find(ShaderStage::Fragment, hash, source, kEnvA);
ASSERT_NE(fragmentHit, nullptr);
EXPECT_EQ(fragmentHit->preprocessedSource, "fragment-preprocessed");
EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source)->preprocessedSource, "vertex-preprocessed");
EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA)->preprocessedSource, "vertex-preprocessed");
EXPECT_EQ(cache.GetEntryCount(), 2u);
}
@@ -2433,27 +2512,27 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheMemoizesRejectionVerdictsDistinctly
const String reservedSource = "int packed;\n";
const String localSizeSource = "layout(local_size_x = 99999) in;\n";
ShaderPreprocessResult reserved;
reserved.outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected;
reserved.infoLog = "reserved identifier";
ShaderPreprocessResult localSize;
localSize.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
localSize.infoLog = "local_size too big";
auto reserved = MakeShared<ShaderPreprocessResult>();
reserved->outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected;
reserved->infoLog = "reserved identifier";
auto localSize = MakeShared<ShaderPreprocessResult>();
localSize->outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
localSize->infoLog = "local_size too big";
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource,
std::move(reserved));
cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource,
std::move(localSize));
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA,
Move(reserved));
cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA,
Move(localSize));
const ShaderPreprocessResult* reservedHit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource);
const ShaderPreprocessResultPtr reservedHit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA);
ASSERT_NE(reservedHit, nullptr);
EXPECT_FALSE(reservedHit->Preprocessed());
EXPECT_EQ(reservedHit->outcome, ShaderPreprocessOutcome::ReservedIdentifierRejected);
EXPECT_EQ(reservedHit->infoLog, "reserved identifier");
const ShaderPreprocessResult* localSizeHit =
cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource);
const ShaderPreprocessResultPtr localSizeHit =
cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA);
ASSERT_NE(localSizeHit, nullptr);
EXPECT_EQ(localSizeHit->outcome, ShaderPreprocessOutcome::ComputeLocalSizeRejected);
EXPECT_EQ(localSizeHit->infoLog, "local_size too big");
@@ -2470,28 +2549,64 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRejectsForgedHashCollision) {
ASSERT_NE(real, impostor);
const Uint64 forgedHash = 0xdeadbeefcafef00dull;
cache.Insert(ShaderStage::Vertex, forgedHash, real, MakeResult("real-preprocessed"));
cache.Insert(ShaderStage::Vertex, forgedHash, real, kEnvA, MakeResult("real-preprocessed"));
ASSERT_NE(cache.Find(ShaderStage::Vertex, forgedHash, real), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor), nullptr);
ASSERT_NE(cache.Find(ShaderStage::Vertex, forgedHash, real, kEnvA), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA), nullptr);
// The colliding newcomer wins the slot rather than being silently dropped, so it
// is the previous occupant that degrades to a miss - never a wrong hit.
cache.Insert(ShaderStage::Vertex, forgedHash, impostor, MakeResult("impostor-preprocessed"));
const ShaderPreprocessResult* impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor);
cache.Insert(ShaderStage::Vertex, forgedHash, impostor, kEnvA, MakeResult("impostor-preprocessed"));
const ShaderPreprocessResultPtr impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA);
ASSERT_NE(impostorHit, nullptr);
EXPECT_EQ(impostorHit->preprocessedSource, "impostor-preprocessed");
EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, real), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, real, kEnvA), nullptr);
EXPECT_EQ(cache.GetEntryCount(), 1u);
}
// P1: the compile environment joins the key. A memo computed against one backend's
// GL_MAX_COMPUTE_WORK_GROUP_* limits must never be handed back after the environment
// changed (backend swap), which is exactly what CompileEnv::fingerprint keys on.
TEST_F(ProgramUtilTest, ShaderPreprocessCacheMissesOnChangedEnvFingerprint) {
ShaderPreprocessCache cache;
const String source = "layout(local_size_x = 512) in;\nvoid main() {}\n";
const Uint64 hash = ShaderPreprocessCache::HashSource(source);
cache.Insert(ShaderStage::Compute, hash, source, kEnvA, MakeResult("env-a-preprocessed"));
ASSERT_NE(cache.Find(ShaderStage::Compute, hash, source, kEnvA), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvB), nullptr);
// Both environments can coexist; neither can see the other's verdict.
cache.Insert(ShaderStage::Compute, hash, source, kEnvB, MakeResult("env-b-preprocessed"));
EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvA)->preprocessedSource, "env-a-preprocessed");
EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvB)->preprocessedSource, "env-b-preprocessed");
EXPECT_EQ(cache.GetEntryCount(), 2u);
}
// A hit hands out shared ownership, so the payload survives the eviction of its entry.
// Under the old raw-pointer API this read was a use-after-free the moment two compiles
// ran concurrently.
TEST_F(ProgramUtilTest, ShaderPreprocessCacheHitOutlivesEviction) {
ShaderPreprocessCache cache;
const String source = "void main() { int keep = 1; }\n";
const Uint64 hash = ShaderPreprocessCache::HashSource(source);
cache.Insert(ShaderStage::Vertex, hash, source, kEnvA, MakeResult("survivor"));
const ShaderPreprocessResultPtr held = cache.Find(ShaderStage::Vertex, hash, source, kEnvA);
ASSERT_NE(held, nullptr);
cache.Clear();
EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA), nullptr);
EXPECT_EQ(held->preprocessedSource, "survivor");
}
TEST_F(ProgramUtilTest, ShaderPreprocessCacheEvictsFifoOnEntryCap) {
ShaderPreprocessCache cache;
Vector<String> sources;
const SizeT overflow = ShaderPreprocessCache::kMaxEntries + 8;
for (SizeT i = 0; i < overflow; ++i) {
sources.push_back("void main() { int a = " + ToString(i) + "; }\n");
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources.back()), sources.back(),
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources.back()), sources.back(), kEnvA,
MakeResult("pp" + ToString(i)));
EXPECT_LE(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries);
}
@@ -2499,8 +2614,8 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheEvictsFifoOnEntryCap) {
// FIFO: the first `overflow - kMaxEntries` insertions are gone, the rest resident.
for (SizeT i = 0; i < overflow; ++i) {
const ShaderPreprocessResult* hit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i]);
const ShaderPreprocessResultPtr hit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i], kEnvA);
if (i < overflow - ShaderPreprocessCache::kMaxEntries) {
EXPECT_EQ(hit, nullptr) << "entry " << i << " should have been evicted";
} else {
@@ -2521,7 +2636,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
const SizeT chunk = ShaderPreprocessCache::kMaxStoredSourceBytes / 8;
for (SizeT i = 0; i < 24; ++i) {
String source(chunk, static_cast<char>('a' + (i % 26)));
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(source), source, MakeResult(""));
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(source), source, kEnvA, MakeResult(""));
EXPECT_LE(cache.GetStoredSourceBytes(), ShaderPreprocessCache::kMaxStoredSourceBytes);
EXPECT_LT(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries);
}
@@ -2530,7 +2645,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
// would evict every other entry and then immediately itself.
const SizeT before = cache.GetEntryCount();
const String oversized(ShaderPreprocessCache::kMaxStoredSourceBytes + 1, 'z');
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, MakeResult(""));
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA, MakeResult(""));
EXPECT_EQ(cache.GetEntryCount(), before);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr);
}
@@ -0,0 +1,98 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/CompileEnv.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
#include "CompileEnv.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
namespace {
void HashBytes(Uint64& state, const void* data, const SizeT length) {
state = static_cast<Uint64>(XXH64(data, length, state));
}
template <typename T>
void HashValue(Uint64& state, const T& value) {
static_assert(std::is_trivially_copyable_v<T>);
HashBytes(state, &value, sizeof(T));
}
} // namespace
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) {
Uint64 state = 0x9e3779b97f4a7c15ull;
HashValue(state, env.maxComputeWorkGroupSize[0]);
HashValue(state, env.maxComputeWorkGroupSize[1]);
HashValue(state, env.maxComputeWorkGroupSize[2]);
HashValue(state, env.maxComputeWorkGroupInvocations);
HashValue(state, env.backend);
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
// representation is deliberate - it means a new limit cannot be added without also
// changing the fingerprint, which is exactly the memo-hazard property wanted here.
HashBytes(state, &env.params, sizeof(env.params));
if (!env.advertisedExtensions.empty()) {
HashBytes(state, env.advertisedExtensions.data(),
env.advertisedExtensions.size() * sizeof(GLExtension));
}
HashValue(state, env.subgroupPrefixScanQuirk);
return state;
}
SharedPtr<const CompileEnv> CaptureCompileEnv() {
auto env = MakeShared<CompileEnv>();
const auto& activeBackend = MG_Backend::pActiveBackendObject;
if (activeBackend) {
env->backend = activeBackend->GetBackendType();
env->params = activeBackend->GetDynamicParameters();
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
}
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must
// happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports.
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
for (Uint index = 0; index < 3; ++index) {
Int backendValue = 0;
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
&backendValue);
}
env->maxComputeWorkGroupSize[index] =
std::max(static_cast<Uint>(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]);
}
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
env->maxComputeWorkGroupInvocations =
activeBackend ? std::max(static_cast<Uint64>(std::max(env->params.MaxComputeWorkGroupInvocations, 0)),
kFrontendMaxComputeWorkGroupInvocations)
: kFrontendMaxComputeWorkGroupInvocations;
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
env->fingerprint = ComputeCompileEnvFingerprint(*env);
return env;
}
const SharedPtr<const CompileEnv>& GetDefaultCompileEnv() {
// Function-local static, not a namespace-scope one: the fingerprint has to be
// computed, and this must not run before MG_Config is loaded.
static const SharedPtr<const CompileEnv> kDefault = [] {
auto env = MakeShared<CompileEnv>();
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
env->fingerprint = ComputeCompileEnvFingerprint(*env);
return SharedPtr<const CompileEnv>(Move(env));
}();
return kDefault;
}
const SharedPtr<const CompileEnv>& GetCurrentCompileEnv() {
if (MG_State::pGLContext) return MG_State::pGLContext->GetCompileEnv();
return GetDefaultCompileEnv();
}
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -0,0 +1,81 @@
// 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 {
// Everything the shader compile/link pipeline reads from OUTSIDE its own (stage, source)
// inputs: backend identity, backend limits, the advertised extension list, and the one
// config quirk the source rewriter branches on.
//
// 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] = {1024, 1024, 64};
// 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;
// --- config the source rewriter branches on ---
MG_Config::QuirkOverride subgroupPrefixScanQuirk = MG_Config::QuirkOverride::Auto;
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; }
// 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);
// 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
@@ -37,7 +37,10 @@
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
TBuiltInResource BuildTBuiltInResource() {
// `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;
@@ -139,7 +142,8 @@ namespace MobileGL {
const MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters =
activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters;
env ? env->params
: (activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters);
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs =
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
@@ -167,7 +171,8 @@ namespace MobileGL {
// copies that could drift apart.
static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
const String& source,
Flags<ShaderCompileBits> flags) {
Flags<ShaderCompileBits> flags,
const CompileEnv* env) {
SharedPtr<glslang::TShader> res;
auto& tshader = res;
tshader = MakeShared<glslang::TShader>(lang);
@@ -194,7 +199,7 @@ namespace MobileGL {
tshader->setAutoMapLocations(true);
tshader->setAutoMapBindings(true);
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
auto resources = BuildTBuiltInResource();
auto resources = BuildTBuiltInResource(env);
if (!tshader->parse(&resources, 460, ECoreProfile,
/*forceDefaultVersionAndProfile: */ false,
/*forwardCompatible: */ true, EShMsgDefault)) {
@@ -220,7 +225,7 @@ namespace MobileGL {
}
const String source(attrib.sourceStr);
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags, attrib.env);
if (result) return result;
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
@@ -236,7 +241,7 @@ namespace MobileGL {
return result;
}
auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags);
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",
@@ -16,6 +16,7 @@
#include <utility>
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include "EsslBuiltinFunctionNames.h"
@@ -1036,15 +1037,6 @@ namespace {
source = std::move(result);
}
bool IsExtensionAdvertised(MobileGL::GLExtension extension) {
const auto& activeBackendObject = MobileGL::MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
return true;
}
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), extension) != extensions.end();
}
MobileGL::String TrimDirectiveToken(const MobileGL::String& token) {
SizeT start = 0;
@@ -1059,8 +1051,9 @@ namespace {
return token.substr(start, end - start);
}
void FilterUnsupportedGpuShaderInt64(MobileGL::String& source) {
if (IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) {
void FilterUnsupportedGpuShaderInt64(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env,
MobileGL::String& source) {
if (env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) {
return;
}
@@ -1314,7 +1307,9 @@ namespace MobileGL {
// instead of open-coding them in PreprocessShaderSource.
struct ShaderSourceQuirk {
const char* name;
MG_Config::QuirkOverride (*GetOverride)();
// Reads the override out of the captured env, never out of the live
// MG_Config table: a worker must see the same config the GL thread saw.
MG_Config::QuirkOverride (*GetOverride)(const CompileEnv&);
Bool (*DeviceApplies)(const ShaderSourceQuirkContext&);
Bool (*Apply)(const ShaderSourceQuirkContext&, String&);
};
@@ -1323,7 +1318,7 @@ namespace MobileGL {
{
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN
"subgroup-prefix-scan-rewrite",
[] { return MG_Config::Features.SubgroupPrefixScanQuirk; },
[](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; },
[](const ShaderSourceQuirkContext& ctx) {
// Qualcomm's Vulkan driver miscompiles the recognized float
// InclusiveScan pattern for native subgroups wider than the
@@ -1339,20 +1334,21 @@ namespace MobileGL {
},
};
void ApplyShaderSourceQuirks(ShaderStage stage, String& source) {
const auto& activeBackend = MG_Backend::pActiveBackendObject;
if (!activeBackend) {
void ApplyShaderSourceQuirks(const CompileEnv& env, ShaderStage stage, String& source) {
// No backend at capture time means no device to match a quirk against,
// and (as before) no quirk can fire - not even a forced one, because
// every Apply reads device parameters that do not exist yet.
if (!env.HasBackend()) {
return;
}
const auto& dynamicParameters = activeBackend->GetDynamicParameters();
const ShaderSourceQuirkContext quirkContext{
stage,
activeBackend->GetBackendType(),
dynamicParameters.GpuVendor,
dynamicParameters.SubgroupSize,
env.backend,
env.params.GpuVendor,
env.params.SubgroupSize,
};
for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) {
const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride();
const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride(env);
if (quirkOverride == MG_Config::QuirkOverride::ForceOff) {
continue;
}
@@ -1369,6 +1365,10 @@ namespace MobileGL {
} // namespace
void PreprocessShaderSource(ShaderStage stage, String& source) {
PreprocessShaderSource(stage, source, *GetCurrentCompileEnv());
}
void PreprocessShaderSource(ShaderStage stage, String& source, const CompileEnv& env) {
// Normalize while the inspector's source span still refers to the untouched input.
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
@@ -1395,7 +1395,7 @@ namespace MobileGL {
// identifier that merely contained the word. The GLES fallback for devices without
// the extension lives in the backend, where device capabilities are known.
FilterUnsupportedGpuShaderInt64(source);
FilterUnsupportedGpuShaderInt64(env, source);
CoerceUniformBlockPackingToStd140(source);
RenameBuiltinShadowingFunctions(source);
@@ -1403,7 +1403,7 @@ namespace MobileGL {
ModernizeLegacyGLSL(stage, source, afterVersion);
InjectDepthRangeBuiltinShim(stage, source, afterVersion);
ApplyShaderSourceQuirks(stage, source);
ApplyShaderSourceQuirks(env, stage, source);
}
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include <MG_State/GLState/ProgramState/ShaderObject.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL {
enum class ShaderProfile {
@@ -19,6 +20,14 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// The whole source-rewriting pipeline. `env` is the compile-time snapshot of
// everything outside (stage, source) this reads - advertised extensions and the
// device-quirk inputs - so the transformation is a pure function of its three
// arguments and can run on a worker thread.
void PreprocessShaderSource(ShaderStage stage, String& source, const CompileEnv& env);
// Convenience overload that resolves the current context's env itself. GL thread
// only, and deliberately not used by the compile pipeline: it exists for the unit
// tests and diagnostics that drive the preprocessor standalone.
void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL {
namespace MG_Util {
@@ -25,6 +26,11 @@ namespace MobileGL {
GLenum shaderType;
StringView sourceStr;
Flags<ShaderCompileBits> flags;
// The compile-time backend snapshot the glslang resource limits come from.
// Null means "read them off the live backend object" - only legal on the GL
// thread, and only used by the standalone/test entry points. Non-owning: the
// env outlives the attrib (it is a per-context SharedPtr).
const CompileEnv* env = nullptr;
};
struct ProgramAttrib {