[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/Classifiers/TextureEnumClassifier.cpp
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
+14
View File
@@ -9,6 +9,8 @@
#include "Core.h" #include "Core.h"
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h" #include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
#include "MG_State/EGLState/Core.h" #include "MG_State/EGLState/Core.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <Config.h> #include <Config.h>
namespace MobileGL::MG_State { namespace MobileGL::MG_State {
@@ -24,6 +26,18 @@ namespace MobileGL::MG_State {
} }
namespace GLState { 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 // Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) { void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
m_errorState.RecordError(code, Move(info)); m_errorState.RecordError(code, Move(info));
+18
View File
@@ -21,6 +21,10 @@
#include "VertexArrayState/VertexArrayState.h" #include "VertexArrayState/VertexArrayState.h"
#include "RenderbufferState/RenderbufferState.h" #include "RenderbufferState/RenderbufferState.h"
namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv;
}
namespace MobileGL { namespace MobileGL {
namespace MG_State { namespace MG_State {
void Init(); void Init();
@@ -380,6 +384,15 @@ namespace MobileGL {
Bool ValidateRenderbufferName(Uint index) const; Bool ValidateRenderbufferName(Uint index) const;
Bool ValidateRenderbufferObject(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: private:
// State Components // State Components
ErrorState m_errorState; ErrorState m_errorState;
@@ -437,6 +450,11 @@ namespace MobileGL {
FramebufferState m_framebufferState; FramebufferState m_framebufferState;
SamplerState m_samplerState; SamplerState m_samplerState;
RenderbufferState m_renderbufferState; 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 } // 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(); Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const; 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 // 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. // is the only place a caller can read it from once the shader name is gone.
void AppendInfoLog(const String& text) { void AppendInfoLog(const String& text) {
if (text.empty()) return; if (text.empty()) return;
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n'; if (!Artifacts().infoLog.empty() && Artifacts().infoLog.back() != '\n') Artifacts().infoLog += '\n';
m_infoLog += text; Artifacts().infoLog += text;
} }
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; } Int GetUniformMaxLength() const { return Artifacts().uniformNameMaxLength; }
Uint GetUniformCount() const { return m_activeUniformCount; } Uint GetUniformCount() const { return Artifacts().activeUniformCount; }
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; } Uint GetMaxUniformLocation() const { return Artifacts().maxUniformLocation; }
Int GetUniformLocation(const String& name) const { Int GetUniformLocation(const String& name) const {
const auto it = m_uniformLocations.find(name); const auto it = Artifacts().uniformLocations.find(name);
if (it != m_uniformLocations.end()) return (Int)it->second; if (it != Artifacts().uniformLocations.end()) return (Int)it->second;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base // 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 // 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. // to base + k because DoReflection reserves one location per array element.
if (name.empty()) return -1; if (name.empty()) return -1;
if (name.back() != ']') { if (name.back() != ']') {
const auto suffixedIt = m_uniformLocations.find(name + "[0]"); const auto suffixedIt = Artifacts().uniformLocations.find(name + "[0]");
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second; if (suffixedIt != Artifacts().uniformLocations.end()) return (Int)suffixedIt->second;
return -1; return -1;
} }
if (name.length() < 4) 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'); element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1; if (element > 0x0FFFFFFFu) return -1;
} }
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]"); auto baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket) + "[0]");
if (baseIt == m_uniformLocations.end()) { if (baseIt == Artifacts().uniformLocations.end()) {
// Legacy key without the "[0]" suffix (defensive; reflection normally // Legacy key without the "[0]" suffix (defensive; reflection normally
// stores the suffixed form for arrays). // stores the suffixed form for arrays).
baseIt = m_uniformLocations.find(name.substr(0, bracket)); baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket));
if (baseIt == m_uniformLocations.end()) return -1; if (baseIt == Artifacts().uniformLocations.end()) return -1;
} }
const Int base = (Int)baseIt->second; const Int base = (Int)baseIt->second;
if (!IsValidUniformLocation(base)) return -1; 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 // "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
// in-range elements. // 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 (type == nullptr || !type->isArray()) return -1;
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1; if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
const Int location = base + (Int)element; 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. // True when both locations are element slots of the same uniform variable.
Bool UniformLocationsAliasSameUniform(Int a, Int b) const { Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false; 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 ---- // ---- 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 // index spaces; every public "index"-taking getter translates through them, so
// GL and backend consumers keep seeing exactly the pre-P0a surface. // GL and backend consumers keep seeing exactly the pre-P0a surface.
Int TProgramUniformIndex(Uint glIndex) const { Int TProgramUniformIndex(Uint glIndex) const {
return m_glUniformIndexToTProgram[glIndex]; return Artifacts().glUniformIndexToTProgram[glIndex];
} }
Int GlUniformIndexFromTProgram(Int tIndex) const { Int GlUniformIndexFromTProgram(Int tIndex) const {
if (tIndex < 0 || tIndex >= static_cast<Int>(m_tProgramUniformIndexToGl.size())) return -1; if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
return m_tProgramUniformIndexToGl[tIndex]; return Artifacts().tProgramUniformIndexToGl[tIndex];
} }
Int GlBlockIndexFromTProgram(Int tBlockIndex) const { Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(m_tProgramBlockIndexToGl.size())) return -1; if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(Artifacts().tProgramBlockIndexToGl.size())) return -1;
return m_tProgramBlockIndexToGl[tBlockIndex]; return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
} }
Int GetActiveUniformIndex(const String& name) const { Int GetActiveUniformIndex(const String& name) const {
const Int tProgramCount = static_cast<Int>(m_tProgramUniformIndexToGl.size()); const Int tProgramCount = static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
const Int uniformIndex = m_program->getUniformIndex(name.c_str()); const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str());
if (uniformIndex >= 0 && uniformIndex < tProgramCount && if (uniformIndex >= 0 && uniformIndex < tProgramCount &&
m_program->getUniform(uniformIndex).name == name) { Artifacts().program->getUniform(uniformIndex).name == name) {
return GlUniformIndexFromTProgram(uniformIndex); return GlUniformIndexFromTProgram(uniformIndex);
} }
@@ -135,9 +135,9 @@ namespace MobileGL::MG_State::GLState {
// robustness against non-suffixed reflection entries. // robustness against non-suffixed reflection entries.
if (!name.empty() && name.back() != ']') { if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]"; 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 && if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
m_program->getUniform(suffixedIndex).name == suffixedName) { Artifacts().program->getUniform(suffixedIndex).name == suffixedName) {
return GlUniformIndexFromTProgram(suffixedIndex); return GlUniformIndexFromTProgram(suffixedIndex);
} }
return -1; 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; if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
const String baseName = name.substr(0, name.length() - 3); 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; 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; : -1;
} }
Bool IsValidUniformLocation(Int location) const { Bool IsValidUniformLocation(Int location) const {
if (location < 0 || location > static_cast<Int>(m_maxUniformLocation)) return false; if (location < 0 || location > static_cast<Int>(Artifacts().maxUniformLocation)) return false;
if (static_cast<SizeT>(location) >= m_uniformIndexInTProgram.size()) return false; if (static_cast<SizeT>(location) >= Artifacts().uniformIndexInTProgram.size()) return false;
const Int uniformIndexInProgram = m_uniformIndexInTProgram[location]; const Int uniformIndexInProgram = Artifacts().uniformIndexInTProgram[location];
return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd && return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd &&
uniformIndexInProgram >= 0 && uniformIndexInProgram >= 0 &&
uniformIndexInProgram < static_cast<Int>(m_tProgramUniformIndexToGl.size()); uniformIndexInProgram < static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
} }
GLenum GetUniformType(Uint location) const { GLenum GetUniformType(Uint location) const {
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
return uniform.glDefineType; return uniform.glDefineType;
} }
GLenum GetActiveUniformType(Uint index) const { GLenum GetActiveUniformType(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
return uniform.glDefineType; 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 // 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 // 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 // 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 { 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(); const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isSizedArray()) { if (type != nullptr && type->isSizedArray()) {
return type->getOuterArraySize(); return type->getOuterArraySize();
@@ -189,7 +189,7 @@ namespace MobileGL::MG_State::GLState {
} }
Int GetActiveUniformBlockIndex(Uint index) const { 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. // Members of the synthesized global UBO are default-block uniforms to GL: -1.
return GlBlockIndexFromTProgram(uniform.index); 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 // 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. // seeing them as default-block uniforms, so gate on the GL-visible block index.
GLint GetActiveUniformOffset(Uint index) const { 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; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
return uniform.offset; 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 // 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. // layout is always std140, where every array element stride rounds up to a vec4.
GLint GetActiveUniformArrayStride(Uint index) const { 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; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType(); const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isArray()) return 0; 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 // check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
// an inheriting member's layoutMatrix == ElmNone. // an inheriting member's layoutMatrix == ElmNone.
GLint GetActiveUniformIsRowMajor(Uint index) const { 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; if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
const glslang::TType* type = uniform.getType(); const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isMatrix()) return 0; if (type == nullptr || !type->isMatrix()) return 0;
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
if (layoutMatrix == glslang::ElmNone) { 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; 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 // 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. // every GL 3.3 float matrix this evaluates to 16, independent of majorness.
GLint GetActiveUniformMatrixStride(Uint index) const { 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; if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType(); const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isMatrix()) return 0; if (type == nullptr || !type->isMatrix()) return 0;
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
if (layoutMatrix == glslang::ElmNone) { 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 bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
@@ -268,50 +268,50 @@ namespace MobileGL::MG_State::GLState {
} }
const glslang::TType* GetUniformTType(Uint location) const { 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(); return uniform.getType();
} }
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); } Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); }
const String& GetUniformName(Uint location) const { 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; return uniform.name;
} }
const String& GetActiveUniformName(Uint index) const { const String& GetActiveUniformName(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
return uniform.name; return uniform.name;
} }
// Sentinel for a uniform location without global-UBO backing storage (should not // Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage). // survive linking: GenerateBinary falls back to tail-allocated scratch storage).
static constexpr Uint kInvalidUniformOffset = ~0u; 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)); } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
Int GetAttributeLocation(const String& name) { Int GetAttributeLocation(const String& name) {
const auto it = std::find(m_attribs.begin(), m_attribs.end(), name); const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name);
return (it == m_attribs.end()) ? -1 : (Int)std::distance(m_attribs.begin(), it); return (it == Artifacts().attribs.end()) ? -1 : (Int)std::distance(Artifacts().attribs.begin(), it);
} }
Uint32 GetActiveAttributeLocationMask() const { Uint32 GetActiveAttributeLocationMask() const {
Uint32 mask = 0; 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) { for (SizeT index = 0; index < count; ++index) {
if (!m_attribs[index].empty()) { if (!Artifacts().attribs[index].empty()) {
mask |= (1u << index); mask |= (1u << index);
} }
} }
return mask; return mask;
} }
Uint32 GetActiveFragmentOutputLocationMask() const { Uint32 GetActiveFragmentOutputLocationMask() const {
if (!m_program) { if (!Artifacts().program) {
return 0; return 0;
} }
Uint32 mask = 0; Uint32 mask = 0;
const Int outputCount = m_program->getNumPipeOutputs(); const Int outputCount = Artifacts().program->getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) { 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) { if (location >= 0 && location < 32) {
mask |= (1u << location); mask |= (1u << location);
} }
@@ -319,38 +319,38 @@ namespace MobileGL::MG_State::GLState {
return mask; return mask;
} }
Int GetActiveFragmentOutputCount() const { Int GetActiveFragmentOutputCount() const {
return m_program ? m_program->getNumPipeOutputs() : 0; return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0;
} }
const String& GetActiveFragmentOutputName(Uint index) const { const String& GetActiveFragmentOutputName(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null"); MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()), MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index); "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 { Int GetFragmentOutputLocation(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null"); MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()), MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputLocation: index=%u out of range", "ProgramObject::GetFragmentOutputLocation: index=%u out of range",
index); 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 { GLint GetActiveFragmentOutputArraySize(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null"); MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()), MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index); "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 { GLenum GetFragmentOutputType(Uint index) const {
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputType: program is null"); MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()), MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
"ProgramObject::GetFragmentOutputType: index=%u out of range", "ProgramObject::GetFragmentOutputType: index=%u out of range",
index); 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]; } GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
const String& GetAttribName(Uint index) const { return m_attribs[index]; } const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
GLenum GetActiveAttribType(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).glDefineType; } GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).glDefineType; }
GLint GetActiveAttribArraySize(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).size; } 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; // 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 // GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
// resource queries enumerate builtins). // resource queries enumerate builtins).
@@ -362,11 +362,11 @@ namespace MobileGL::MG_State::GLState {
return name; return name;
} }
const String& GetActiveAttribName(Uint index) const { 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(); } void* MapUBO() { return Artifacts().globalUboScratch.data(); }
const void* GetUBOData() const { return m_globalUboScratch.data(); } const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
Uint GetUBOSize() const { return static_cast<Uint>(m_globalUboScratch.size()); } Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
// Content version of the CPU-side global-UBO shadow: writers bump it so backends // 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 // 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. // 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) { void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
if (location >= m_uniformSamplerOrImageUnitIndex.size() || if (location >= Artifacts().uniformSamplerOrImageUnitIndex.size() ||
m_uniformSamplerOrImageUnitIndex[location] == unit) { Artifacts().uniformSamplerOrImageUnitIndex[location] == unit) {
return; return;
} }
m_uniformSamplerOrImageUnitIndex[location] = unit; Artifacts().uniformSamplerOrImageUnitIndex[location] = unit;
++m_backendStateVersion; ++m_backendStateVersion;
} }
Int GetUniformSamplerOrImageUnitIndex(Uint location) const { Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
return m_uniformSamplerOrImageUnitIndex[location]; return Artifacts().uniformSamplerOrImageUnitIndex[location];
} }
Bool GetDeleteStatus() const { return m_deleteStatus; } 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_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 // (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
// ARB_get_program_binary requires of it. // 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 // glProgramBinary always fails here (there is no format it could accept) and the
// spec then requires the program's LINK_STATUS to read FALSE. // spec then requires the program's LINK_STATUS to read FALSE.
void MarkLinkFailedByProgramBinary() { void MarkLinkFailedByProgramBinary() {
BumpLinkObservableVersions();
ResetLinkArtifacts(); ResetLinkArtifacts();
m_infoLog = "No program binary format is supported."; Artifacts().infoLog = "No program binary format is supported.";
} }
Bool GetValidateStatus() const { return m_validateStatus; } Bool GetValidateStatus() const { return m_validateStatus; }
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); } Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); }
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); } Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); }
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse // GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
// materializes for default-block uniforms is filtered out by DoReflection. // materializes for default-block uniforms is filtered out by DoReflection.
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(m_glBlockIndexToTProgram.size()); } Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
GLuint GetComputeLocalSize(Uint dim) const { return m_program->getLocalSize(static_cast<Int>(dim)); } GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast<Int>(dim)); }
Int GetActiveAttributesMaxLength() const { return m_attribInNameMaxLength; } Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
Int GetActiveUniformBlocksMaxNameLength() const { return m_uniformBlockNameMaxLength; } Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
Uint GetUniformBlockIndex(const char* name) const { Uint GetUniformBlockIndex(const char* name) const {
auto it = m_uniformBlockIndexByName.find(name); auto it = Artifacts().uniformBlockIndexByName.find(name);
if (it != m_uniformBlockIndexByName.end()) return it->second; if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]"; // 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. // a bare "Block" query resolves to the first instance per GL semantics.
const String suffixedName = String(name) + "[0]"; const String suffixedName = String(name) + "[0]";
it = m_uniformBlockIndexByName.find(suffixedName); it = Artifacts().uniformBlockIndexByName.find(suffixedName);
if (it != m_uniformBlockIndexByName.end()) return it->second; if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
return 0xFFFFFFFFu; // GL_INVALID_INDEX return 0xFFFFFFFFu; // GL_INVALID_INDEX
} }
Bool IsActiveUniformBlock(Uint index) const { 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 // (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 // 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). // 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 { 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; return ubo.name;
} }
@@ -487,8 +488,8 @@ namespace MobileGL::MG_State::GLState {
if (name.empty() || name.back() != ']') return index; if (name.empty() || name.back() != ']') return index;
const SizeT bracket = name.rfind('['); const SizeT bracket = name.rfind('[');
if (bracket == String::npos) return index; if (bracket == String::npos) return index;
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]"); const auto it = Artifacts().uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
if (it != m_uniformBlockIndexByName.end()) return it->second; if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
return index; return index;
} }
@@ -499,31 +500,31 @@ namespace MobileGL::MG_State::GLState {
Int GetUniformBlockActiveUniformCount(Uint index) const { Int GetUniformBlockActiveUniformCount(Uint index) const {
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index)); const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
Int count = 0; Int count = 0;
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) { for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) {
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count; if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
} }
return count; return count;
} }
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const { 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); const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
return (ubo.stages & stageMask) != 0; return (ubo.stages & stageMask) != 0;
} }
// Set by glUniformBlockBinding // Set by glUniformBlockBinding
void SetUniformBlockBinding(Uint index, Uint binding) { 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; return;
} }
m_uniformBlockBinding[index] = static_cast<Int>(binding); Artifacts().uniformBlockBinding[index] = static_cast<Int>(binding);
++m_backendStateVersion; ++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; } Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return m_generatedSpirv; } const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
Int GetShaderIndexByStage(ShaderStage stage) const { Int GetShaderIndexByStage(ShaderStage stage) const {
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) { 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. // layout captures into; see NeedsScatteredTransformFeedbackCapture.
Uint32 packedOffsetBytes = 0; 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) { void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
m_requestedXfbVaryings = Move(names); m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode; m_requestedXfbBufferMode = bufferMode;
} }
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; } GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); } SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const { 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. // Stride of one captured vertex in the given capture buffer slot.
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const { 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(); } SizeT GetTransformFeedbackBufferCount() const { return Artifacts().xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; } Int GetTransformFeedbackVaryingMaxLength() const { return Artifacts().xfbVaryingNameMaxLength; }
// True when the capture layout uses gl_SkipComponents / gl_NextBuffer // True when the capture layout uses gl_SkipComponents / gl_NextBuffer
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every // (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 // 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 // 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 // application's buffers itself, using packedOffsetBytes as the source offset and
// (bufferIndex, offsetBytes, stride) as the destination. // (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. // 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 // 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 // statically-known emit sequence: the Vulkan capture order then needs the GL
// odd-triangle vertex swap after EndTransformFeedback. // 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. // 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_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 // 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. // 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; } Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL // 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; } Uint64 GetLifetimeId() const { return m_lifetimeId; }
private: 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(); 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 // 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. // 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 // Resolves the requested transform feedback varyings against the linked
// vertex stage; fails the link (GL semantics) on unknown or duplicate // vertex stage; fails the link (GL semantics) on unknown or duplicate
// names or exceeded capture limits. // names or exceeded capture limits.
Bool ResolveTransformFeedbackVaryings(); Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate); void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
// The former GenerateBinary, split around DoReflection's data dependencies: // 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 // live-variable analysis mutates the intermediates enough to change
// GlslangToSpv output), while the glUniform*-to-global-UBO routing tables are // GlslangToSpv output), while the glUniform*-to-global-UBO routing tables are
// sized and keyed by reflection results (m_maxUniformLocation, // sized and keyed by reflection results (maxUniformLocation, uniformLocations)
// m_uniformLocations) and so must run AFTER it. // and so must run AFTER it.
void GenerateSpirv(); void GenerateSpirv();
void BuildGlobalUboRouting(); void BuildGlobalUboRouting();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing(); void AddDefaultFragmentShaderIfMissing();
Bool ValidateFragmentOutputLocations(); Bool ValidateFragmentOutputLocations();
static Uint64 AllocateLifetimeId(); 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 Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = 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_shaders;
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
SharedPtr<glslang::TProgram> m_program; // Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation,
// glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer
Vector<Vector<unsigned>> m_generatedSpirv; // count stamped in by the entry point. A pending link snapshots these at enqueue.
// Attributes (Vertex in)
UnorderedMap<String, Uint> m_explicitAttribLocations; 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_explicitFragDataLocation;
UnorderedMap<String, Uint> m_linkedFragDataLocation;
// Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted // Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted
// into the linked map at link time, like the location maps above. // into the linked map at link time, like the location maps above.
UnorderedMap<String, Uint> m_explicitFragDataIndex; UnorderedMap<String, Uint> m_explicitFragDataIndex;
UnorderedMap<String, Uint> m_linkedFragDataIndex;
Int m_maxFragmentOutputColorNumber = 8; 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_deleteStatus = false;
Bool m_linkStatus = false;
Bool m_binaryRetrievableHint = false; Bool m_binaryRetrievableHint = false;
Bool m_separable = false; Bool m_separable = false;
Bool m_validateStatus = true; Bool m_validateStatus = true;
@@ -704,17 +781,9 @@ namespace MobileGL::MG_State::GLState {
Uint32 m_uboContentVersion = 0; Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0; Uint32 m_linkVersion = 0;
// Transform feedback: request (applies at next link) and linked snapshot. // ---- Link OUTPUT ----
Vector<String> m_requestedXfbVaryings; // Written by the link and by the post-link setters GL allows (glUniform1i's sampler
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS; // unit, glUniformBlockBinding). Reachable only through Artifacts(); see LinkArtifacts.
Vector<XfbVarying> m_xfbVaryings; LinkArtifacts m_artifacts;
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;
}; };
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -79,7 +79,7 @@ namespace MobileGL::MG_State::GLState {
Uint shaderId = 0; Uint shaderId = 0;
m_programShaderNameGenerator.Generate(1, &shaderId); m_programShaderNameGenerator.Generate(1, &shaderId);
EnsureIndexAvail(shaderId, m_shaderObjects); 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; if (shaderObject == nullptr) return 0;
m_shaderObjects[shaderId] = shaderObject; m_shaderObjects[shaderId] = shaderObject;
return shaderId; 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 // 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 // directly - shader objects reach it through the pointer they are handed at
// CreateShader(). // CreateShader().
ShaderPreprocessCache& GetShaderPreprocessCache() { return m_shaderPreprocessCache; } ShaderPreprocessCache& GetShaderPreprocessCache() { return *m_shaderPreprocessCache; }
private: private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const; 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. // object kinds keeps the names disjoint; the object tables stay separate.
IndexGenerator<Uint> m_programShaderNameGenerator; IndexGenerator<Uint> m_programShaderNameGenerator;
// P0b layer 2: every shader object created here is handed a pointer to this cache. // P0b layer 2: every shader object created here is handed shared ownership of this
// Declared FIRST on purpose - members are destroyed in reverse declaration order, // cache, so its lifetime no longer depends on member destruction order (P1: an
// so the cache outlives every shader object holding a pointer to it. // in-flight compile job may outlive the context). The FIRST-member declaration is
ShaderPreprocessCache m_shaderPreprocessCache; // kept anyway - it costs nothing and documents the intent.
SharedPtr<ShaderPreprocessCache> m_shaderPreprocessCache = MakeShared<ShaderPreprocessCache>();
Vector<SharedPtr<ProgramObject>> m_programObjects; Vector<SharedPtr<ProgramObject>> m_programObjects;
Vector<SharedPtr<ShaderObject>> m_shaderObjects; Vector<SharedPtr<ShaderObject>> m_shaderObjects;
@@ -12,8 +12,8 @@
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h> #include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h> #include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
#include <MG_Backend/BackendObjects.h>
#include <charconv> #include <charconv>
@@ -115,41 +115,22 @@ namespace {
return localSize; return localSize;
} }
static MobileGL::Uint GetComputeWorkGroupSizeLimit(MobileGL::Uint index) { // The device limits come from the CompileEnv snapshot, never from a live driver query.
constexpr MobileGL::Uint kFrontendMinComputeWorkGroupSizes[] = {1024, 1024, 64}; // GL_MAX_COMPUTE_WORK_GROUP_SIZE is a real GLES call on the DirectGLES backend: issued
MobileGL::Int backendValue = 0; // off the context thread it would silently no-op and turn a legal local_size_z into
if (MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) { // COMPILE_STATUS=FALSE. CaptureCompileEnv() issues it once, on the GL thread.
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, static std::optional<MobileGL::String> ValidateComputeLocalSizeLimits(
&backendValue); const MobileGL::String& source, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
}
// 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) {
const ComputeLocalSize localSize = ParseComputeLocalSize(source); const ComputeLocalSize localSize = ParseComputeLocalSize(source);
if (!localSize.declared) return std::nullopt; if (!localSize.declared) return std::nullopt;
if (localSize.x > GetComputeWorkGroupSizeLimit(0) || localSize.y > GetComputeWorkGroupSizeLimit(1) || if (localSize.x > env.maxComputeWorkGroupSize[0] || localSize.y > env.maxComputeWorkGroupSize[1] ||
localSize.z > GetComputeWorkGroupSizeLimit(2)) { localSize.z > env.maxComputeWorkGroupSize[2]) {
return "Compute shader local_size exceeds GL_MAX_COMPUTE_WORK_GROUP_SIZE."; 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; 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."; 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 // 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. // 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 // The former caveat is gone: the compute local-size verdict reads `env` rather than the
// reads the active backend's GL_MAX_COMPUTE_WORK_GROUP_* limits. Those are fixed for // live backend, and env.fingerprint is part of the P0b cache key, so a memo can never be
// the lifetime of a context, and the cache is per-context, so the memo cannot outlive // returned against limits other than the ones it was computed against.
// the limits it was computed against.
static MobileGL::MG_State::GLState::ShaderPreprocessResult RunSourceOnlyPipeline( 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;
using namespace MobileGL::MG_Util::ShaderTranspiler; using namespace MobileGL::MG_Util::ShaderTranspiler;
using MobileGL::MG_State::GLState::ShaderPreprocessOutcome; using MobileGL::MG_State::GLState::ShaderPreprocessOutcome;
MobileGL::MG_State::GLState::ShaderPreprocessResult result; MobileGL::MG_State::GLState::ShaderPreprocessResult result;
result.preprocessedSource = source; result.preprocessedSource = source;
PreprocessShaderSource(stage, result.preprocessedSource); PreprocessShaderSource(stage, result.preprocessedSource, env);
if (stage == ShaderStage::Compute) { if (stage == ShaderStage::Compute) {
if (const std::optional<String> localSizeError = if (const std::optional<String> localSizeError =
ValidateComputeLocalSizeLimits(result.preprocessedSource)) { ValidateComputeLocalSizeLimits(result.preprocessedSource, env)) {
result.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected; result.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
result.infoLog = *localSizeError; result.infoLog = *localSizeError;
return result; return result;
@@ -240,14 +221,14 @@ namespace MobileGL::MG_State::GLState {
m_compiledSourceLength = m_source.length(); 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() { void ShaderObject::InvalidateCompiledState() {
m_shader.reset(); // The compile artifacts are exactly what one Compile() writes, so discarding them
m_preprocessedSource.clear(); // wholesale IS the invalidation. (Kept as an explicit reset rather than a
m_explicitUniformLocations.clear(); // default-construct so the intent survives a future field addition.)
m_explicitOpaqueBindings.clear(); Compiled() = CompileArtifacts{};
m_shaderConsumedByLink = false;
m_compileStatus = false;
m_infoLog.clear();
m_hasCompiledState = false; m_hasCompiledState = false;
m_compiledSourceHash = 0; m_compiledSourceHash = 0;
m_compiledSourceLength = 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 // 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. // failure case too - the info log stays queryable because nothing is cleared.
// //
// m_shaderConsumedByLink interaction: if the stored TShader already fed a link, // shaderConsumedByLink interaction: if the stored TShader already fed a link,
// the no-op leaves m_preprocessedSource and both side-channel maps intact, which // 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 // 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 // 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. // 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); 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 // P0b layer 2: another shader object in this context may already have run the
// source-only half over byte-identical text. // source-only half over byte-identical text under the same environment.
const ShaderPreprocessResult* cached = ShaderPreprocessResultPtr cached =
m_preprocessCache != nullptr ? m_preprocessCache->Find(m_stage, sourceHash, m_source) : nullptr; m_preprocessCache ? m_preprocessCache->Find(m_stage, sourceHash, m_source, env.fingerprint) : nullptr;
ShaderPreprocessResult fresh; SharedPtr<ShaderPreprocessResult> fresh;
if (cached == nullptr) fresh = RunSourceOnlyPipeline(m_stage, m_source); if (!cached) fresh = MakeShared<ShaderPreprocessResult>(RunSourceOnlyPipeline(m_stage, m_source, env));
const ShaderPreprocessResult& shared = cached != nullptr ? *cached : fresh; const ShaderPreprocessResult& shared = cached ? *cached : *fresh;
const Bool shouldPopulateCache = cached == nullptr && m_preprocessCache != nullptr; const Bool shouldPopulateCache = !cached && m_preprocessCache != nullptr;
if (!shared.Preprocessed()) { if (!shared.Preprocessed()) {
// Rejected lexically, or a glslang failure this context has already seen for // Rejected lexically, or a glslang failure this context has already seen for
// this exact source (ParseFailed) - either way the parse can be skipped. // this exact source (ParseFailed) - either way the parse can be skipped.
m_infoLog = shared.infoLog; compiled.infoLog = shared.infoLog;
if (shouldPopulateCache) m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh)); if (shouldPopulateCache) {
m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh));
}
RememberCompiledSource(sourceHash); RememberCompiledSource(sourceHash);
return; return;
} }
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage), ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
.sourceStr = shared.preprocessedSource, .sourceStr = shared.preprocessedSource,
.flags = 0}; .flags = 0,
.env = &env};
auto result = ShaderCompiler::CompileShader(attrib); auto result = ShaderCompiler::CompileShader(attrib);
if (result) { if (result) {
m_compileStatus = true; compiled.compileStatus = true;
m_shader = result.value(); compiled.shader = result.value();
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and // Copy, not move: `shared` may alias a cache entry that has to outlive us, and
// `fresh` is about to be handed to the cache. // `fresh` is about to be handed to the cache.
m_preprocessedSource = shared.preprocessedSource; compiled.preprocessedSource = shared.preprocessedSource;
m_explicitUniformLocations = shared.explicitUniformLocations; compiled.explicitUniformLocations = shared.explicitUniformLocations;
m_explicitOpaqueBindings = shared.explicitOpaqueBindings; compiled.explicitOpaqueBindings = shared.explicitOpaqueBindings;
m_infoLog.clear(); compiled.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());
if (shouldPopulateCache) { if (shouldPopulateCache) {
fresh.outcome = ShaderPreprocessOutcome::ParseFailed; m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh));
fresh.infoLog = m_infoLog; }
fresh.explicitUniformLocations.clear(); } else {
fresh.explicitOpaqueBindings.clear(); compiled.infoLog = result.error().log;
m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh)); 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); RememberCompiledSource(sourceHash);
} }
SharedPtr<glslang::TShader> ShaderObject::TakeShaderForLink(String& outReparseLog) { SharedPtr<glslang::TShader> ShaderObject::TakeShaderForLink(String& outReparseLog) {
if (m_shader && !m_shaderConsumedByLink) { CompileArtifacts& compiled = Compiled();
m_shaderConsumedByLink = true; if (compiled.shader && !compiled.shaderConsumedByLink) {
return m_shader; compiled.shaderConsumedByLink = true;
return compiled.shader;
} }
// The stored parse already fed a link, whose mapIO mutated its intermediate. // 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. // here on EVERY link rather than only on reuse.
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage), ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
.sourceStr = m_preprocessedSource, .sourceStr = compiled.preprocessedSource,
.flags = 0}; .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); auto result = ShaderCompiler::CompileShader(attrib);
if (!result) { if (!result) {
// Should be unreachable: the same source parsed successfully at Compile(). // Should be unreachable: the same source parsed successfully at Compile().
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL { namespace MobileGL {
enum class ShaderStage { enum class ShaderStage {
@@ -31,9 +32,12 @@ namespace MobileGL {
// `preprocessCache` is the owning context's cross-object memo (P0b layer 2); // `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 // 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. // 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, ShaderObject(const ShaderStage stage, Uint externalIndex,
ShaderPreprocessCache* preprocessCache = nullptr) SharedPtr<ShaderPreprocessCache> preprocessCache = nullptr)
: m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(preprocessCache) {} : m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(Move(preprocessCache)) {}
void SetShaderSource(const String& source); void SetShaderSource(const String& source);
void SetShaderSource(String&& source); void SetShaderSource(String&& source);
void Compile(); void Compile();
@@ -51,29 +55,83 @@ namespace MobileGL {
Uint GetExternalIndex() const { return m_externalIndex; } Uint GetExternalIndex() const { return m_externalIndex; }
ShaderStage GetShaderStage() const { return m_stage; } ShaderStage GetShaderStage() const { return m_stage; }
const String& GetShaderSource() const { return m_source; } const String& GetShaderSource() const { return m_source; }
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return m_shader; } const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return m_infoLog; } const String& GetInfoLog() const { return Compiled().infoLog; }
const UnorderedMap<String, Uint>& GetUniformLocations() const { return m_uniforms; } const UnorderedMap<String, Uint>& GetUniformLocations() const { return Compiled().uniforms; }
// Explicit layout(location = N) qualifiers on this shader's default-block // Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, captured lexically at Compile() because the relaxed parse drops // uniforms, captured lexically at Compile() because the relaxed parse drops
// them from reflection (see ExtractExplicitUniformLocations). // them from reflection (see ExtractExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const { const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return m_explicitUniformLocations; return Compiled().explicitUniformLocations;
} }
// Explicit layout(binding = N) on sampler/image uniforms - their initial // Explicit layout(binding = N) on sampler/image uniforms - their initial
// texture/image units - captured lexically for the same reason (see // texture/image units - captured lexically for the same reason (see
// ExtractExplicitOpaqueBindings). // ExtractExplicitOpaqueBindings).
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const { return m_explicitOpaqueBindings; } const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
Bool GetCompileStatus() const { return m_compileStatus; } return Compiled().explicitOpaqueBindings;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; } 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 // 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 // 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 // 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. // 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; } Bool HasMemoizedCompile() const { return m_hasCompiledState; }
private: 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(); void InvalidateCompiledState();
// ---- P0b layer 1: per-object no-op recompile ---- // ---- P0b layer 1: per-object no-op recompile ----
// True iff `candidate` is byte-identical to the source that produced the // 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. // Arms the layer-1 memo for the source that Compile() just processed.
void RememberCompiledSource(Uint64 sourceHash); void RememberCompiledSource(Uint64 sourceHash);
// ---- GL-thread-owned state: never produced by a compile, so it never joins ----
const Uint m_externalIndex = 0; const Uint m_externalIndex = 0;
const ShaderStage m_stage; 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; 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. // P0b layer 2: the owning context's cross-object memo, or null.
ShaderPreprocessCache* const m_preprocessCache = nullptr; const SharedPtr<ShaderPreprocessCache> m_preprocessCache;
// P0b layer 1. m_hasCompiledState is the invariant "m_source is byte-identical // 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 // to the source that produced the compile artifacts"; it is armed at the end of
// at the end of every Compile() and disarmed by InvalidateCompiledState(). // 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; Bool m_hasCompiledState = false;
Uint64 m_compiledSourceHash = 0; Uint64 m_compiledSourceHash = 0;
SizeT m_compiledSourceLength = 0; SizeT m_compiledSourceLength = 0;
String m_infoLog;
Bool m_deleteStatus = false; Bool m_deleteStatus = false;
Bool m_compileStatus = false;
// ---- Compile OUTPUT ---- reachable only through Compiled().
CompileArtifacts m_compiled;
}; };
} // namespace MG_State::GLState } // namespace MG_State::GLState
} // namespace MobileGL } // namespace MobileGL
@@ -9,9 +9,14 @@
#include "ShaderPreprocessCache.h" #include "ShaderPreprocessCache.h"
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
const ShaderPreprocessResult* ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash, ShaderPreprocessResultPtr ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash,
const String& source) const { const String& source, const Uint64 envFingerprint) const {
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);
const auto it = m_index.find(key); const auto it = m_index.find(key);
if (it == m_index.end()) return nullptr; if (it == m_index.end()) return nullptr;
@@ -20,52 +25,62 @@ namespace MobileGL::MG_State::GLState {
const Entry& entry = *it->second; const Entry& entry = *it->second;
if (entry.originalSource != source) return nullptr; 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, void ShaderPreprocessCache::Insert(const ShaderStage stage, const Uint64 sourceHash, const String& source,
ShaderPreprocessResult result) { const Uint64 envFingerprint, ShaderPreprocessResultPtr result) {
const SizeT entryBytes = EntryBytes(source, result); if (!result) return;
const SizeT entryBytes = EntryBytes(source, *result);
// A single source bigger than the whole budget would evict every other entry and // A single source bigger than the whole budget would evict every other entry and
// then itself; refuse it instead of thrashing the cache empty. // then itself; refuse it instead of thrashing the cache empty.
if (entryBytes > kMaxStoredSourceBytes) return; 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()) { 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 // 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 // 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 // entry per key keeps the index a plain map, and a collision is astronomically
// rare enough that the loser simply misses. // 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_entries.push_back(Entry{.key = key, .originalSource = source, .result = Move(result)});
m_index[key] = std::prev(m_entries.end()); m_index[key] = std::prev(m_entries.end());
m_storedSourceBytes += entryBytes; m_storedSourceBytes += entryBytes;
EvictUntilWithinBudget(); EvictUntilWithinBudgetLocked();
} }
void ShaderPreprocessCache::Clear() { void ShaderPreprocessCache::Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_entries.clear(); m_entries.clear();
m_index.clear(); m_index.clear();
m_storedSourceBytes = 0; m_storedSourceBytes = 0;
} }
void ShaderPreprocessCache::EraseEntry(const EntryList::iterator it) { void ShaderPreprocessCache::EraseEntryLocked(const EntryList::iterator it) {
const SizeT bytes = EntryBytes(it->originalSource, it->result); const SizeT bytes = EntryBytes(it->originalSource, *it->result);
m_storedSourceBytes = bytes > m_storedSourceBytes ? 0 : m_storedSourceBytes - bytes; m_storedSourceBytes = bytes > m_storedSourceBytes ? 0 : m_storedSourceBytes - bytes;
m_index.erase(it->key); m_index.erase(it->key);
m_entries.erase(it); m_entries.erase(it);
} }
void ShaderPreprocessCache::EvictUntilWithinBudget() { void ShaderPreprocessCache::EvictUntilWithinBudgetLocked() {
// FIFO: the oldest insertion goes first. Insert() already refuses entries larger // 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 // than the byte budget, so this loop always terminates with at least the entry
// that was just added still resident. // that was just added still resident.
while (!m_entries.empty() && while (!m_entries.empty() &&
(m_entries.size() > kMaxEntries || m_storedSourceBytes > kMaxStoredSourceBytes)) { (m_entries.size() > kMaxEntries || m_storedSourceBytes > kMaxStoredSourceBytes)) {
EraseEntry(m_entries.begin()); EraseEntryLocked(m_entries.begin());
} }
} }
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <list> #include <list>
#include <mutex>
#include <MG_State/GLState/ProgramState/ShaderObject.h> #include <MG_State/GLState/ProgramState/ShaderObject.h>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
@@ -42,6 +43,11 @@ namespace MobileGL::MG_State::GLState {
Bool Preprocessed() const { return outcome == ShaderPreprocessOutcome::Preprocessed; } 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 // P0b layer 2: a per-context, bounded memo of the source-only half of shader
// compilation, keyed by (stage, xxhash64(source), source length). // 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 kMaxEntries = 128;
static constexpr SizeT kMaxStoredSourceBytes = 8u * 1024u * 1024u; static constexpr SizeT kMaxStoredSourceBytes = 8u * 1024u * 1024u;
// Returns the memoized result for this exact source, or null on a miss. The // Returns the memoized result for this exact source under this exact compile
// returned pointer stays valid until the next Insert()/Clear() on this cache. // environment, or null on a miss. The returned SharedPtr owns its payload, so it
const ShaderPreprocessResult* Find(ShaderStage stage, Uint64 sourceHash, const String& source) const; // 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 // 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 // exceeds the byte budget is simply not cached (caching it would evict everything
// else and then itself). // 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(); void Clear();
@@ -86,17 +100,25 @@ namespace MobileGL::MG_State::GLState {
return static_cast<Uint64>(XXH64(source.data(), source.length(), 0)); return static_cast<Uint64>(XXH64(source.data(), source.length(), 0));
} }
SizeT GetEntryCount() const { return m_entries.size(); } SizeT GetEntryCount() const {
SizeT GetStoredSourceBytes() const { return m_storedSourceBytes; } 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: private:
struct Key { struct Key {
ShaderStage stage = ShaderStage::Unknown; ShaderStage stage = ShaderStage::Unknown;
Uint64 sourceHash = 0; Uint64 sourceHash = 0;
SizeT sourceLength = 0; SizeT sourceLength = 0;
Uint64 envFingerprint = 0;
Bool operator==(const Key& other) const { 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; Uint64 mixed = key.sourceHash;
mixed ^= static_cast<Uint64>(key.sourceLength) + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2); mixed ^= static_cast<Uint64>(key.sourceLength) + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
mixed ^= static_cast<Uint64>(static_cast<Int>(key.stage)) * 0xff51afd7ed558ccdull; mixed ^= static_cast<Uint64>(static_cast<Int>(key.stage)) * 0xff51afd7ed558ccdull;
mixed ^= key.envFingerprint + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
return static_cast<SizeT>(mixed); 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 // The full original (pre-preprocess) source, kept so a hit can be confirmed by
// comparison instead of trusting the hash. // comparison instead of trusting the hash.
String originalSource; String originalSource;
ShaderPreprocessResult result; ShaderPreprocessResultPtr result;
}; };
using EntryList = std::list<Entry>; using EntryList = std::list<Entry>;
@@ -125,14 +148,16 @@ namespace MobileGL::MG_State::GLState {
return source.length() + result.preprocessedSource.length(); return source.length() + result.preprocessedSource.length();
} }
void EraseEntry(EntryList::iterator it); void EvictUntilWithinBudgetLocked();
void EvictUntilWithinBudget();
// P1: needs a mutex when compiles go async. Everything here is reached from void EraseEntryLocked(EntryList::iterator it);
// 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 // P1: every public entry point takes this. The lock alone would NOT have been
// worker pool, Find/Insert/Clear all become critical sections (and Find's returned // enough - the old Find() handed back a raw pointer into an entry that a
// pointer stops being safe to hold across an Insert). // 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) EntryList m_entries; // front = oldest (FIFO victim)
UnorderedMap<Key, EntryList::iterator, KeyHasher> m_index; UnorderedMap<Key, EntryList::iterator, KeyHasher> m_index;
SizeT m_storedSourceBytes = 0; SizeT m_storedSourceBytes = 0;
+154 -39
View File
@@ -21,6 +21,7 @@
#include <MG_Util/ShaderTranspiler/Types.h> #include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h> #include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h> #include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <spirv-tools/libspirv.hpp> #include <spirv-tools/libspirv.hpp>
#include <spirv-tools/optimizer.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 // P0b layer 2: ShaderPreprocessCache, tested directly. The GL-level behaviour it
// enables is covered end to end in ProgramTest; these pin the container itself, // enables is covered end to end in ProgramTest; these pin the container itself,
// where the interesting cases (hash collisions, both eviction budgets) are hard // 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::ShaderPreprocessCache;
using MobileGL::MG_State::GLState::ShaderPreprocessOutcome; using MobileGL::MG_State::GLState::ShaderPreprocessOutcome;
using MobileGL::MG_State::GLState::ShaderPreprocessResult; using MobileGL::MG_State::GLState::ShaderPreprocessResult;
using MobileGL::MG_State::GLState::ShaderPreprocessResultPtr;
ShaderPreprocessResult MakeResult(const String& preprocessed) { // The env fingerprint every test below keys against, unless it is specifically
ShaderPreprocessResult result; // exercising the fingerprint itself.
result.outcome = ShaderPreprocessOutcome::Preprocessed; constexpr MobileGL::Uint64 kEnvA = 0x1111'2222'3333'4444ull;
result.preprocessedSource = preprocessed; constexpr MobileGL::Uint64 kEnvB = 0x5555'6666'7777'8888ull;
result.explicitUniformLocations["uMarker"] = 7;
result.explicitOpaqueBindings["sMarker"] = 3; 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; return result;
} }
} // namespace } // namespace
@@ -2403,10 +2482,10 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) {
const String source = "// a shader\nvoid main() {}\n"; const String source = "// a shader\nvoid main() {}\n";
const Uint64 hash = ShaderPreprocessCache::HashSource(source); 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")); cache.Insert(ShaderStage::Vertex, hash, source, kEnvA, MakeResult("vertex-preprocessed"));
const ShaderPreprocessResult* hit = cache.Find(ShaderStage::Vertex, hash, source); const ShaderPreprocessResultPtr hit = cache.Find(ShaderStage::Vertex, hash, source, kEnvA);
ASSERT_NE(hit, nullptr); ASSERT_NE(hit, nullptr);
EXPECT_TRUE(hit->Preprocessed()); EXPECT_TRUE(hit->Preprocessed());
EXPECT_EQ(hit->preprocessedSource, "vertex-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 // 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. // stages sharing one entry would hand a fragment shader a vertex preprocess.
EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source), nullptr); EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source, kEnvA), nullptr);
cache.Insert(ShaderStage::Fragment, hash, source, MakeResult("fragment-preprocessed")); cache.Insert(ShaderStage::Fragment, hash, source, kEnvA, MakeResult("fragment-preprocessed"));
const ShaderPreprocessResult* fragmentHit = cache.Find(ShaderStage::Fragment, hash, source); const ShaderPreprocessResultPtr fragmentHit = cache.Find(ShaderStage::Fragment, hash, source, kEnvA);
ASSERT_NE(fragmentHit, nullptr); ASSERT_NE(fragmentHit, nullptr);
EXPECT_EQ(fragmentHit->preprocessedSource, "fragment-preprocessed"); 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); EXPECT_EQ(cache.GetEntryCount(), 2u);
} }
@@ -2433,27 +2512,27 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheMemoizesRejectionVerdictsDistinctly
const String reservedSource = "int packed;\n"; const String reservedSource = "int packed;\n";
const String localSizeSource = "layout(local_size_x = 99999) in;\n"; const String localSizeSource = "layout(local_size_x = 99999) in;\n";
ShaderPreprocessResult reserved; auto reserved = MakeShared<ShaderPreprocessResult>();
reserved.outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected; reserved->outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected;
reserved.infoLog = "reserved identifier"; reserved->infoLog = "reserved identifier";
ShaderPreprocessResult localSize; auto localSize = MakeShared<ShaderPreprocessResult>();
localSize.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected; localSize->outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
localSize.infoLog = "local_size too big"; localSize->infoLog = "local_size too big";
cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA,
std::move(reserved)); Move(reserved));
cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA,
std::move(localSize)); Move(localSize));
const ShaderPreprocessResult* reservedHit = const ShaderPreprocessResultPtr reservedHit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource); cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA);
ASSERT_NE(reservedHit, nullptr); ASSERT_NE(reservedHit, nullptr);
EXPECT_FALSE(reservedHit->Preprocessed()); EXPECT_FALSE(reservedHit->Preprocessed());
EXPECT_EQ(reservedHit->outcome, ShaderPreprocessOutcome::ReservedIdentifierRejected); EXPECT_EQ(reservedHit->outcome, ShaderPreprocessOutcome::ReservedIdentifierRejected);
EXPECT_EQ(reservedHit->infoLog, "reserved identifier"); EXPECT_EQ(reservedHit->infoLog, "reserved identifier");
const ShaderPreprocessResult* localSizeHit = const ShaderPreprocessResultPtr localSizeHit =
cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource); cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA);
ASSERT_NE(localSizeHit, nullptr); ASSERT_NE(localSizeHit, nullptr);
EXPECT_EQ(localSizeHit->outcome, ShaderPreprocessOutcome::ComputeLocalSizeRejected); EXPECT_EQ(localSizeHit->outcome, ShaderPreprocessOutcome::ComputeLocalSizeRejected);
EXPECT_EQ(localSizeHit->infoLog, "local_size too big"); EXPECT_EQ(localSizeHit->infoLog, "local_size too big");
@@ -2470,28 +2549,64 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRejectsForgedHashCollision) {
ASSERT_NE(real, impostor); ASSERT_NE(real, impostor);
const Uint64 forgedHash = 0xdeadbeefcafef00dull; 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); ASSERT_NE(cache.Find(ShaderStage::Vertex, forgedHash, real, kEnvA), nullptr);
EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor), nullptr); EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA), nullptr);
// The colliding newcomer wins the slot rather than being silently dropped, so it // 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. // is the previous occupant that degrades to a miss - never a wrong hit.
cache.Insert(ShaderStage::Vertex, forgedHash, impostor, MakeResult("impostor-preprocessed")); cache.Insert(ShaderStage::Vertex, forgedHash, impostor, kEnvA, MakeResult("impostor-preprocessed"));
const ShaderPreprocessResult* impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor); const ShaderPreprocessResultPtr impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA);
ASSERT_NE(impostorHit, nullptr); ASSERT_NE(impostorHit, nullptr);
EXPECT_EQ(impostorHit->preprocessedSource, "impostor-preprocessed"); 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); 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) { TEST_F(ProgramUtilTest, ShaderPreprocessCacheEvictsFifoOnEntryCap) {
ShaderPreprocessCache cache; ShaderPreprocessCache cache;
Vector<String> sources; Vector<String> sources;
const SizeT overflow = ShaderPreprocessCache::kMaxEntries + 8; const SizeT overflow = ShaderPreprocessCache::kMaxEntries + 8;
for (SizeT i = 0; i < overflow; ++i) { for (SizeT i = 0; i < overflow; ++i) {
sources.push_back("void main() { int a = " + ToString(i) + "; }\n"); 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))); MakeResult("pp" + ToString(i)));
EXPECT_LE(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries); 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. // FIFO: the first `overflow - kMaxEntries` insertions are gone, the rest resident.
for (SizeT i = 0; i < overflow; ++i) { for (SizeT i = 0; i < overflow; ++i) {
const ShaderPreprocessResult* hit = const ShaderPreprocessResultPtr hit =
cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i]); cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i], kEnvA);
if (i < overflow - ShaderPreprocessCache::kMaxEntries) { if (i < overflow - ShaderPreprocessCache::kMaxEntries) {
EXPECT_EQ(hit, nullptr) << "entry " << i << " should have been evicted"; EXPECT_EQ(hit, nullptr) << "entry " << i << " should have been evicted";
} else { } else {
@@ -2521,7 +2636,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
const SizeT chunk = ShaderPreprocessCache::kMaxStoredSourceBytes / 8; const SizeT chunk = ShaderPreprocessCache::kMaxStoredSourceBytes / 8;
for (SizeT i = 0; i < 24; ++i) { for (SizeT i = 0; i < 24; ++i) {
String source(chunk, static_cast<char>('a' + (i % 26))); 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_LE(cache.GetStoredSourceBytes(), ShaderPreprocessCache::kMaxStoredSourceBytes);
EXPECT_LT(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries); EXPECT_LT(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries);
} }
@@ -2530,7 +2645,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
// would evict every other entry and then immediately itself. // would evict every other entry and then immediately itself.
const SizeT before = cache.GetEntryCount(); const SizeT before = cache.GetEntryCount();
const String oversized(ShaderPreprocessCache::kMaxStoredSourceBytes + 1, 'z'); 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.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 MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { 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{}; TBuiltInResource Resources{};
Resources.maxLights = 32; Resources.maxLights = 32;
Resources.maxClipPlanes = 6; Resources.maxClipPlanes = 6;
@@ -139,7 +142,8 @@ namespace MobileGL {
const MG_Backend::DynamicBackendParameters fallbackParameters{}; const MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject; const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters = const auto& dynamicParameters =
activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters; env ? env->params
: (activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters);
Resources.maxImageUnits = dynamicParameters.MaxImageUnits; Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs = Resources.maxCombinedImageUnitsAndFragmentOutputs =
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
@@ -167,7 +171,8 @@ namespace MobileGL {
// copies that could drift apart. // copies that could drift apart.
static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType, static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
const String& source, const String& source,
Flags<ShaderCompileBits> flags) { Flags<ShaderCompileBits> flags,
const CompileEnv* env) {
SharedPtr<glslang::TShader> res; SharedPtr<glslang::TShader> res;
auto& tshader = res; auto& tshader = res;
tshader = MakeShared<glslang::TShader>(lang); tshader = MakeShared<glslang::TShader>(lang);
@@ -194,7 +199,7 @@ namespace MobileGL {
tshader->setAutoMapLocations(true); tshader->setAutoMapLocations(true);
tshader->setAutoMapBindings(true); tshader->setAutoMapBindings(true);
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
auto resources = BuildTBuiltInResource(); auto resources = BuildTBuiltInResource(env);
if (!tshader->parse(&resources, 460, ECoreProfile, if (!tshader->parse(&resources, 460, ECoreProfile,
/*forceDefaultVersionAndProfile: */ false, /*forceDefaultVersionAndProfile: */ false,
/*forwardCompatible: */ true, EShMsgDefault)) { /*forwardCompatible: */ true, EShMsgDefault)) {
@@ -220,7 +225,7 @@ namespace MobileGL {
} }
const String source(attrib.sourceStr); 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; if (result) return result;
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the // Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
@@ -236,7 +241,7 @@ namespace MobileGL {
return result; return result;
} }
auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags); auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags, attrib.env);
if (!retryResult) return result; if (!retryResult) return result;
MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460", MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460",
@@ -16,6 +16,7 @@
#include <utility> #include <utility>
#include <Config.h> #include <Config.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include "EsslBuiltinFunctionNames.h" #include "EsslBuiltinFunctionNames.h"
@@ -1036,15 +1037,6 @@ namespace {
source = std::move(result); 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) { MobileGL::String TrimDirectiveToken(const MobileGL::String& token) {
SizeT start = 0; SizeT start = 0;
@@ -1059,8 +1051,9 @@ namespace {
return token.substr(start, end - start); return token.substr(start, end - start);
} }
void FilterUnsupportedGpuShaderInt64(MobileGL::String& source) { void FilterUnsupportedGpuShaderInt64(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env,
if (IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) { MobileGL::String& source) {
if (env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) {
return; return;
} }
@@ -1314,7 +1307,9 @@ namespace MobileGL {
// instead of open-coding them in PreprocessShaderSource. // instead of open-coding them in PreprocessShaderSource.
struct ShaderSourceQuirk { struct ShaderSourceQuirk {
const char* name; 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 (*DeviceApplies)(const ShaderSourceQuirkContext&);
Bool (*Apply)(const ShaderSourceQuirkContext&, String&); Bool (*Apply)(const ShaderSourceQuirkContext&, String&);
}; };
@@ -1323,7 +1318,7 @@ namespace MobileGL {
{ {
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN // MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN
"subgroup-prefix-scan-rewrite", "subgroup-prefix-scan-rewrite",
[] { return MG_Config::Features.SubgroupPrefixScanQuirk; }, [](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; },
[](const ShaderSourceQuirkContext& ctx) { [](const ShaderSourceQuirkContext& ctx) {
// Qualcomm's Vulkan driver miscompiles the recognized float // Qualcomm's Vulkan driver miscompiles the recognized float
// InclusiveScan pattern for native subgroups wider than the // InclusiveScan pattern for native subgroups wider than the
@@ -1339,20 +1334,21 @@ namespace MobileGL {
}, },
}; };
void ApplyShaderSourceQuirks(ShaderStage stage, String& source) { void ApplyShaderSourceQuirks(const CompileEnv& env, ShaderStage stage, String& source) {
const auto& activeBackend = MG_Backend::pActiveBackendObject; // No backend at capture time means no device to match a quirk against,
if (!activeBackend) { // 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; return;
} }
const auto& dynamicParameters = activeBackend->GetDynamicParameters();
const ShaderSourceQuirkContext quirkContext{ const ShaderSourceQuirkContext quirkContext{
stage, stage,
activeBackend->GetBackendType(), env.backend,
dynamicParameters.GpuVendor, env.params.GpuVendor,
dynamicParameters.SubgroupSize, env.params.SubgroupSize,
}; };
for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) { 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) { if (quirkOverride == MG_Config::QuirkOverride::ForceOff) {
continue; continue;
} }
@@ -1369,6 +1365,10 @@ namespace MobileGL {
} // namespace } // namespace
void PreprocessShaderSource(ShaderStage stage, String& source) { 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. // Normalize while the inspector's source span still refers to the untouched input.
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source); const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
@@ -1395,7 +1395,7 @@ namespace MobileGL {
// identifier that merely contained the word. The GLES fallback for devices without // identifier that merely contained the word. The GLES fallback for devices without
// the extension lives in the backend, where device capabilities are known. // the extension lives in the backend, where device capabilities are known.
FilterUnsupportedGpuShaderInt64(source); FilterUnsupportedGpuShaderInt64(env, source);
CoerceUniformBlockPackingToStd140(source); CoerceUniformBlockPackingToStd140(source);
RenameBuiltinShadowingFunctions(source); RenameBuiltinShadowingFunctions(source);
@@ -1403,7 +1403,7 @@ namespace MobileGL {
ModernizeLegacyGLSL(stage, source, afterVersion); ModernizeLegacyGLSL(stage, source, afterVersion);
InjectDepthRangeBuiltinShim(stage, source, afterVersion); InjectDepthRangeBuiltinShim(stage, source, afterVersion);
ApplyShaderSourceQuirks(stage, source); ApplyShaderSourceQuirks(env, stage, source);
} }
Bool RetargetLegacyVersionDirectiveTo460(String& source) { Bool RetargetLegacyVersionDirectiveTo460(String& source) {
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/ProgramState/ShaderObject.h> #include <MG_State/GLState/ProgramState/ShaderObject.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL { namespace MobileGL {
enum class ShaderProfile { enum class ShaderProfile {
@@ -19,6 +20,14 @@ namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { 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); void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan // Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
@@ -25,6 +26,11 @@ namespace MobileGL {
GLenum shaderType; GLenum shaderType;
StringView sourceStr; StringView sourceStr;
Flags<ShaderCompileBits> flags; 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 { struct ProgramAttrib {