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