[Perf] (MG_State, MG_Util): compile shaders with a single relaxed parse

glCompileShader used to parse every source twice: once under the GL client
(reflection only) and once under the relaxed Vulkan client (SPIR-V + the
plain-uniform global UBO), with GenerateBinary re-preprocessing, re-parsing
and re-linking every attached shader on every glLinkProgram. The GL-client
pass is gone: Compile() performs the one link-compatible relaxed parse and
the linked TProgram serves reflection and codegen both. Measured on the BSL
shaderpack compile phase: Espryt 2.80s -> 2.14s, Magma 3.78s -> 3.07s.

What the relaxed parse cannot provide is restored explicitly:
- explicit layout(location/binding) qualifiers on default-block uniforms and
  samplers are extracted lexically at Compile() (the relaxed parse strips
  them) and merged per link with cross-stage conflict checks;
- uniforms the relaxed parse sweeps into MGL_GLOBAL_UBO but no stage reads
  are filtered from the GL reflection surface through GL<->TProgram index
  translation maps (dead uniforms stay inactive, the synthesized block stays
  hidden, builtins reflect under their GL spellings);
- SPIR-V is generated BEFORE buildReflection touches the program (its
  live-variable analysis perturbs GlslangToSpv output - generated modules
  stay bit-identical to the old pipeline's), while the glUniform*-to-scratch
  routing tables are built strictly AFTER reflection, whose results size and
  key them;
- a TShader feeds exactly one link (mapIO mutates the intermediate); relinks
  and multi-program attachments re-parse the stored preprocessed source.

Validated: DirectGLES retrace suite green (two pre-existing local-driver
failures unchanged old vs new), KHR-GL30 877/878 on Espryt/NVIDIA (the one
failure pre-exists this change), unit tests green, per-module SPIR-V hashes
identical across a full DirectVulkan replay.
This commit is contained in:
BZLZHH
2026-08-08 01:25:54 -04:00
parent 81bcbd6c14
commit 0d0527192a
7 changed files with 983 additions and 128 deletions
@@ -155,6 +155,11 @@ namespace MobileGL::MG_State::GLState {
m_program.reset();
m_generatedSpirv.clear();
m_uniformLocations.clear();
m_glUniformIndexToTProgram.clear();
m_tProgramUniformIndexToGl.clear();
m_glBlockIndexToTProgram.clear();
m_tProgramBlockIndexToGl.clear();
m_linkedExplicitUniformLocations.clear();
m_uniformIndexInTProgram.clear();
m_uniformSamplerOrImageUnitIndex.clear();
m_explicitOpaqueUniformBindings.clear();
@@ -564,13 +569,45 @@ namespace MobileGL::MG_State::GLState {
MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str());
return;
}
shaders[i] = m_shaders[i]->GetCompiledShader();
String reparseLog;
shaders[i] = m_shaders[i]->TakeShaderForLink(reparseLog);
if (!shaders[i]) {
// Only reachable when the consume-once re-parse of an already-compiled
// source fails, which no valid state transition produces.
m_infoLog = std::format("Internal error: re-parsing an attached {} for linking failed:\n{}",
MG_Util::ConvertGLEnumToString(shaderTypes[i]), reparseLog);
MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: shader[%zu] compiled shader ptr %p, src len %zu", m_externalIndex, i,
shaders[i].get(), m_shaders[i]->GetShaderSource().length());
MGLOG_D("ProgramObject %u: shader[%zu] source:\n%s", m_externalIndex, i,
m_shaders[i]->GetShaderSource().c_str());
}
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : m_shaders) {
for (const auto& [name, location] : shader->GetExplicitUniformLocations()) {
const auto [it, inserted] = m_linkedExplicitUniformLocations.emplace(name, location);
if (!inserted && it->second != location) {
m_infoLog = std::format(
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
"across stages.",
name, it->second, location);
MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str());
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
for (const auto& [name, binding] : shader->GetExplicitOpaqueBindings()) {
m_explicitOpaqueUniformBindings[name] = binding;
}
}
MG_Util::ShaderTranspiler::ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = m_explicitAttribLocations,
.explicitFragmentOutLocations = m_explicitFragDataLocation,
@@ -606,8 +643,27 @@ namespace MobileGL::MG_State::GLState {
}
}
// SPIR-V must be generated BEFORE buildReflection touches m_program:
// reflection's live-variable analysis mutates the intermediates in ways that
// change subsequent GlslangToSpv output (observed: catastrophic uniform
// misbinding on DirectVulkan for UBO-heavy content). The old two-link pipeline
// never ran buildReflection on the SPIR-V-producing program; this order keeps
// that property with the single link. The glUniform*-to-scratch routing
// tables, in contrast, are sized and keyed by reflection results, so they are
// built strictly AFTER DoReflection. (Everything else on the reflection
// surface - locations, sampler units, block bindings/sizes - was measured
// identical in either order.)
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", m_externalIndex);
GenerateSpirv();
MGLOG_D("ProgramObject %u: Starting reflection", m_externalIndex);
DoReflection();
if (!DoReflection()) {
MGLOG_E("ProgramObject %u: Link failed during reflection: %s", m_externalIndex, m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", m_externalIndex);
BuildGlobalUboRouting();
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)m_linkStatus);
if (!ValidateFragmentOutputLocations()) {
return;
@@ -618,9 +674,6 @@ namespace MobileGL::MG_State::GLState {
m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
GenerateBinary();
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", m_externalIndex,
m_generatedSpirv.size());
}
@@ -642,12 +695,12 @@ namespace MobileGL::MG_State::GLState {
return m_shaders;
}
void ProgramObject::DoReflection() {
Bool ProgramObject::DoReflection() {
if (!m_program) {
MGLOG_E("ProgramObject %u: DoReflection called but m_program is null", m_externalIndex);
m_linkStatus = false;
m_infoLog = "DoReflection failed: no program.";
return;
return false;
}
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
@@ -666,26 +719,124 @@ namespace MobileGL::MG_State::GLState {
m_linkStatus = false;
m_infoLog = "Build reflection failed.";
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
return;
return false;
}
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
// Blocks first: global-UBO membership drives the uniform filter below. The
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
// default-block uniforms and the block itself must stay invisible to GL (it
// did not exist in the GL-client parse this replaces).
const Int tProgramBlockCount = m_program->getNumUniformBlocks();
m_tProgramBlockIndexToGl.assign(tProgramBlockCount, -1);
m_glBlockIndexToTProgram.clear();
for (Int i = 0; i < tProgramBlockCount; i++) {
const auto& ubo = m_program->getUniformBlock(i);
if (std::strstr(ubo.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
continue;
}
m_tProgramBlockIndexToGl[i] = static_cast<Int>(m_glBlockIndexToTProgram.size());
m_glBlockIndexToTProgram.push_back(i);
}
// ------------ Uniforms (GL Plain) ----------------
// Allocate uniform locations
m_activeUniformCount = m_program->getNumUniformVariables();
// The relaxed parse sweeps every DECLARED default-block uniform into
// MGL_GLOBAL_UBO whether or not any stage reads it. GL requires a
// declared-but-unreferenced default-block uniform to be inactive (absent from
// glGetActiveUniform, glGetUniformLocation == -1): filter global-UBO members no
// stage references. Named-block members keep GL's every-declared-member-is-active
// semantics, exactly as before.
const Int tProgramUniformCount = m_program->getNumUniformVariables();
m_tProgramUniformIndexToGl.assign(tProgramUniformCount, -1);
m_glUniformIndexToTProgram.clear();
const auto isGlobalUboMember = [this](const glslang::TObjectReflection& uniform) {
return uniform.index >= 0 && uniform.index < static_cast<Int>(m_tProgramBlockIndexToGl.size()) &&
m_tProgramBlockIndexToGl[uniform.index] < 0;
};
for (Int i = 0; i < tProgramUniformCount; i++) {
const auto& uniform = m_program->getUniform(i);
if (isGlobalUboMember(uniform) && uniform.stages == 0) {
MGLOG_D("ProgramObject %u: Reflection - dead default-block uniform '%s' filtered from the GL "
"surface",
m_externalIndex, uniform.name.c_str());
continue;
}
m_tProgramUniformIndexToGl[i] = static_cast<Int>(m_glUniformIndexToTProgram.size());
m_glUniformIndexToTProgram.push_back(i);
}
m_activeUniformCount = static_cast<Uint>(m_glUniformIndexToTProgram.size());
MGLOG_D("ProgramObject %u: Reflection - active uniform count = %d (of %d reflected)", m_externalIndex,
m_activeUniformCount, tProgramUniformCount);
// Effective explicit location per TProgram uniform, from two sources:
// - the lexical side-channel for default-block uniforms - the relaxed parse
// dropped their layout(location = N) qualifiers when collecting them into
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
// survives the relaxed parse (and mapIO auto-assigns the rest).
constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd;
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
const auto findExplicitLocation = [this](const String& reflectedName) -> const Int* {
auto it = m_linkedExplicitUniformLocations.find(reflectedName);
if (it == m_linkedExplicitUniformLocations.end() && reflectedName.length() > 3 &&
reflectedName.compare(reflectedName.length() - 3, 3, "[0]") == 0) {
it = m_linkedExplicitUniformLocations.find(reflectedName.substr(0, reflectedName.length() - 3));
}
return it != m_linkedExplicitUniformLocations.end() ? &it->second : nullptr;
};
for (const Int i : m_glUniformIndexToTProgram) {
const auto& uniform = m_program->getUniform(i);
const glslang::TType* type = uniform.getType();
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
if (inNamedBlock) continue; // block members never take glUniform locations
if (const Int* explicitLocation = findExplicitLocation(uniform.name)) {
effectiveLocation[i] = static_cast<Uint>(*explicitLocation);
locationIsSourceExplicit[i] = true;
} else if (!m_linkedExplicitUniformLocations.empty() &&
uniform.name.find('.') != String::npos) {
// A struct uniform's explicit location spreads consecutively over its
// flattened members ("s.a", "s[1].b", ...) in reflection order.
const SizeT cut = uniform.name.find_first_of(".[");
const auto rootIt = m_linkedExplicitUniformLocations.find(uniform.name.substr(0, cut));
if (rootIt != m_linkedExplicitUniformLocations.end()) {
auto [cursor, inserted] =
structExplicitCursor.emplace(rootIt->first, static_cast<Uint>(rootIt->second));
(void)inserted;
effectiveLocation[i] = cursor->second;
locationIsSourceExplicit[i] = true;
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
}
}
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) {
effectiveLocation[i] = uniform.layoutLocation();
}
if (locationIsSourceExplicit[i] &&
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kNoLocation) {
// Config A rejected out-of-range explicit locations at parse; keep them
// from growing the location table unboundedly.
m_infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
effectiveLocation[i]);
ResetLinkArtifacts();
return false;
}
}
Int requiredUniformLocations = 0;
MGLOG_D("ProgramObject %u: Reflection - active uniform count = %d", m_externalIndex, m_activeUniformCount);
for (int i = 0; i < m_activeUniformCount; i++) {
for (const Int i : m_glUniformIndexToTProgram) {
auto& uniform = m_program->getUniform(i);
auto location = uniform.layoutLocation();
const Uint location = effectiveLocation[i];
const Int locationSpan = GetUniformLocationSpan(uniform);
requiredUniformLocations += locationSpan;
if (location != glslang::TQualifier::layoutLocationEnd) {
if (location != kNoLocation) {
m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1);
}
m_uniformNameMaxLength = std::max(m_uniformNameMaxLength, (Int)uniform.name.length());
m_uniformLocations[uniform.name] = location;
MGLOG_D("ProgramObject %u: Reflection - uniform[%d] name='%s' layoutLocation=%d", m_externalIndex, i,
uniform.name.c_str(), location);
MGLOG_D("ProgramObject %u: Reflection - uniform[%d] name='%s' effectiveLocation=%d", m_externalIndex,
i, uniform.name.c_str(), location);
}
MGLOG_D("ProgramObject %u: Reflection - computed m_maxUniformLocation=%u m_uniformNameMaxLength=%d",
@@ -706,21 +857,62 @@ namespace MobileGL::MG_State::GLState {
Vector<int> unallocatedUniformIndex;
// Populate vector with already allocated location
for (int i = 0; i < m_activeUniformCount; i++) {
// Pass 1: source-explicit locations. These are API contract
// (ARB_explicit_uniform_location), and an overlap between distinct uniforms is a
// link error - config A's mapIO rejected it ("Uniform location overlaps across
// stages"); the relaxed parse dropped the qualifiers, so it is enforced here.
for (const Int i : m_glUniformIndexToTProgram) {
auto& uniform = m_program->getUniform(i);
auto location = uniform.layoutLocation();
if (m_uniformLocations[uniform.name] == glslang::TQualifier::layoutLocationEnd) {
if (!locationIsSourceExplicit[i] || effectiveLocation[i] == kNoLocation) continue;
const Uint location = effectiveLocation[i];
const Int locationSpan = GetUniformLocationSpan(uniform);
for (Int element = 0; element < locationSpan; ++element) {
const Int existing = m_uniformIndexInTProgram[location + element];
if (existing != glslang::TQualifier::layoutLocationEnd && existing != i) {
m_infoLog =
std::format("Uniform location overlap: '{}' and '{}' both occupy location {}.",
m_program->getUniform(existing).name, uniform.name, location + element);
ResetLinkArtifacts();
return false;
}
m_uniformIndexInTProgram[location + element] = i;
}
MGLOG_D("ProgramObject %u: Reflection - assigned explicit-location uniform '%s' to locations "
"%u..%u (indexInTProgram=%d)",
m_externalIndex, uniform.name.c_str(), location, location + locationSpan - 1, i);
}
// Pass 2: glslang-assigned locations (opaque uniforms under the relaxed parse).
// Implementation-chosen, so on a collision with an explicit location the uniform
// is demoted to the first-fit pass below instead of failing the link.
for (const Int i : m_glUniformIndexToTProgram) {
auto& uniform = m_program->getUniform(i);
if (locationIsSourceExplicit[i]) continue;
const Uint location = effectiveLocation[i];
if (location == kNoLocation) {
unallocatedUniformIndex.emplace_back(i);
MGLOG_D("ProgramObject %u: Reflection - uniform '%s' is unallocated, will assign later",
m_externalIndex, uniform.name.c_str());
continue; // will allocate unallocated uniforms later
}
const Int locationSpan = GetUniformLocationSpan(uniform);
Bool spanIsFree = location + locationSpan - 1 <= m_maxUniformLocation;
for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
spanIsFree =
m_uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd;
}
if (!spanIsFree) {
m_uniformLocations[uniform.name] = kNoLocation;
unallocatedUniformIndex.emplace_back(i);
MGLOG_D("ProgramObject %u: Reflection - uniform '%s' auto location %u collides with an "
"explicit location, demoting to first-fit",
m_externalIndex, uniform.name.c_str(), location);
continue;
}
for (Int element = 0; element < locationSpan; ++element) {
m_uniformIndexInTProgram[location + element] = i;
}
MGLOG_D("ProgramObject %u: Reflection - assigned uniform '%s' to locations %d..%d "
MGLOG_D("ProgramObject %u: Reflection - assigned uniform '%s' to locations %u..%u "
"(indexInTProgram=%d)",
m_externalIndex, uniform.name.c_str(), location, location + locationSpan - 1, i);
}
@@ -772,7 +964,7 @@ namespace MobileGL::MG_State::GLState {
}
}
for (int i = 0; i < m_activeUniformCount; i++) {
for (const Int i : m_glUniformIndexToTProgram) {
auto& uniform = m_program->getUniform(i);
const auto locationIt = m_uniformLocations.find(uniform.name);
if (locationIt == m_uniformLocations.end()) {
@@ -842,7 +1034,10 @@ namespace MobileGL::MG_State::GLState {
for (int i = 0; i < inCount; ++i) {
auto& inVar = m_program->getPipeInput(i);
Int location = (Int)inVar.layoutLocation();
m_attribInNameMaxLength = std::max(m_attribInNameMaxLength, (Int)inVar.name.length());
// Builtins reflect under their SPIR-V names here; GL_ACTIVE_ATTRIBUTE_MAX_LENGTH
// must measure the GL spelling glGetActiveAttrib will report.
m_attribInNameMaxLength =
std::max(m_attribInNameMaxLength, (Int)NormalizeBuiltinPipeInputName(inVar.name).length());
if (location >= 0 && location < (int)m_attribs.size()) {
const Int locationSpan = GetVertexInputLocationSpan(inVar.glDefineType);
@@ -868,11 +1063,12 @@ namespace MobileGL::MG_State::GLState {
}
// ---------- UBO ----------
Int uboCount = m_program->getNumUniformBlocks();
// GL-visible blocks only (MGL_GLOBAL_UBO was filtered out above).
const Int uboCount = GetActiveUniformBlocksCount();
MGLOG_D("ProgramObject %u: Reflection - uniform block count (UBO) = %d", m_externalIndex, uboCount);
m_uniformBlockBinding.resize(uboCount, -1);
for (int i = 0; i < uboCount; i++) {
auto& ubo = m_program->getUniformBlock(i);
for (Int i = 0; i < uboCount; i++) {
auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[i]);
m_uniformBlockNameMaxLength = std::max(m_uniformBlockNameMaxLength, (Int)ubo.name.length());
m_uniformBlockIndexByName[ubo.name] = i;
// if there's binding defined in shader as layout(binding = ...),
@@ -881,82 +1077,56 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", m_externalIndex, i,
ubo.name.c_str(), ubo.size, ubo.getBinding());
}
return true;
}
void ProgramObject::GenerateBinary() {
void ProgramObject::GenerateSpirv() {
/* As we passed first stage compilation/linking,
* we'll assume all the operations here should
* pass. We may be able to employ some optimizations
* here without the burden of error reporting.
*/
using namespace MG_Util::ShaderTranspiler;
MGLOG_D("ProgramObject %u: GenerateBinary - start", m_externalIndex);
Vector<SharedPtr<glslang::TShader>> shaders(m_shaders.size());
MGLOG_D("ProgramObject %u: GenerateSpirv - start", m_externalIndex);
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
// configuration, and m_program linked those parses - so m_program IS the
// program the backends consume. Generate SPIR-V straight from its
// intermediates; the full re-parse + re-link that used to live here (one
// glslang pass per shader per link) is gone.
Vector<GLenum> shaderTypes(m_shaders.size());
// 1. Compile shaders
for (SizeT i = 0; i < m_shaders.size(); i++) {
auto shaderStage = m_shaders[i]->GetShaderStage();
auto shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderStage);
String compileSource = m_shaders[i]->GetShaderSource();
PreprocessShaderSource(shaderStage, compileSource);
shaderTypes[i] = shaderType;
ShaderAttrib attrib{.shaderType = shaderType,
.sourceStr = compileSource,
.flags = 0}; // Will need patched glslang to work
MGLOG_D("ProgramObject %u: GenerateBinary - compiling shader[%zu] type %u", m_externalIndex, i, shaderType);
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
MGLOG_E("ProgramObject %u: GenerateBinary - CompileShader failed for shader[%zu], aborting "
"binary generation",
m_externalIndex, i);
MGLOG_E("ProgramObject %u: GenerateBinary - CompileShader return code %d, log:\n%s", m_externalIndex,
res.error().errc, res.error().log.c_str());
MGLOG_E("ProgramObject %u: GenerateBinary - last compiled shader src: \n%s", m_externalIndex,
compileSource.c_str());
}
MOBILEGL_ASSERT(res, "CompileShader failed during binary generation");
shaders[i] = res.value();
MGLOG_D("ProgramObject %u: GenerateBinary - compiled shader[%zu] -> TShader ptr %p", m_externalIndex, i,
shaders[i].get());
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(m_shaders[i]->GetShaderStage());
}
// 2. Do actual linking
ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = m_explicitAttribLocations,
.explicitFragmentOutLocations = m_explicitFragDataLocation,
.explicitFragmentOutIndices = m_explicitFragDataIndex,
.explicitOpaqueUniformBindings = &m_explicitOpaqueUniformBindings};
MGLOG_D("ProgramObject %u: GenerateBinary - linking program for binary", m_externalIndex);
auto programResult = ShaderCompiler::LinkProgram(attrib);
if (!programResult) {
MGLOG_E("ProgramObject %u: GenerateBinary - LinkProgram failed during binary generation", m_externalIndex);
}
MOBILEGL_ASSERT(programResult, "LinkProgram failed during binary generation");
auto& program = programResult.value();
MGLOG_D("ProgramObject %u: GenerateBinary - got linked program object", m_externalIndex);
ProgramBinaryAttrib binaryAttrib{
.shaderTypes = shaderTypes,
.program = *program,
.program = *m_program,
};
MGLOG_D("ProgramObject %u: GenerateBinary - requesting SPIR-V binary from program", m_externalIndex);
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", m_externalIndex);
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binaryResult) {
MGLOG_E("ProgramObject %u: GenerateBinary - GetSpirvBinaryFromProgram failed", m_externalIndex);
MGLOG_E("ProgramObject %u: GenerateSpirv - GetSpirvBinaryFromProgram failed", m_externalIndex);
}
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
m_generatedSpirv = Move(binaryResult.value());
MGLOG_D("ProgramObject %u: GenerateBinary - generated %zu SPIR-V modules", m_externalIndex,
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", m_externalIndex,
m_generatedSpirv.size());
// 3. Linked SPIR-V generated, sanitize and optimize it
// Linked SPIR-V generated, sanitize and optimize it
for (auto& spv : m_generatedSpirv) {
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
}
}
void ProgramObject::BuildGlobalUboRouting() {
using namespace MG_Util::ShaderTranspiler;
Vector<GLenum> shaderTypes(m_shaders.size());
for (SizeT i = 0; i < m_shaders.size(); i++) {
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(m_shaders[i]->GetShaderStage());
}
// 4. Do reflection (find global UBO etc.)
m_uniformSizesInBytes.clear();
m_uniformOffsets.clear();
m_globalUboScratch.clear();
@@ -969,13 +1139,13 @@ namespace MobileGL::MG_State::GLState {
auto& spv = m_generatedSpirv[i];
auto shaderType = shaderTypes[i];
MGLOG_D("ProgramObject %u: GenerateBinary - parsing SPIR-V meta data for module %zu "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
"(shaderType=%u, wordCount=%zu)",
m_externalIndex, i, shaderType, spv.size());
SpvcSession session(spv, SessionUsageBit::Reflection);
auto result = session.ParseMetaData();
if (result < 0) {
MGLOG_D("ProgramObject %u: GenerateBinary - SpvcSession::ParseMetaData failed for module %zu, "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
"err = %d%s",
m_externalIndex, i, result,
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
@@ -983,7 +1153,7 @@ namespace MobileGL::MG_State::GLState {
} else {
auto& meta = session.GetMetadata();
auto size = meta.globalUboSize;
MGLOG_D("ProgramObject %u: GenerateBinary - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
"plainUniformOffsets=%zu",
m_externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
meta.plainUniformOffsetsInUBO.size());
@@ -1002,7 +1172,7 @@ namespace MobileGL::MG_State::GLState {
locationIt = m_uniformLocations.find(name + "[0]");
}
if (locationIt == m_uniformLocations.end()) {
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
"m_uniformLocations",
m_externalIndex, name.c_str(), offset);
continue;
@@ -1013,7 +1183,7 @@ namespace MobileGL::MG_State::GLState {
}
const Int uniformIndex = m_uniformIndexInTProgram[baseLocation];
const GLint arraySize = GetActiveUniformArraySize(uniformIndex);
const GLint arraySize = GetUniformArraySizeByTIndex(uniformIndex);
SizeT memberSize = 0;
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
@@ -1037,12 +1207,12 @@ namespace MobileGL::MG_State::GLState {
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
}
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u size=%zu assigned "
"to locations %u..%u",
m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
baseLocation + static_cast<Uint>(elementCount) - 1);
}
MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata",
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
m_externalIndex, i);
}
}
@@ -1079,7 +1249,7 @@ namespace MobileGL::MG_State::GLState {
m_globalUboScratch.resize(slotOffset + slotSize, 0);
m_uniformOffsets[location] = static_cast<Uint>(slotOffset);
m_uniformSizesInBytes[location] = slotSize;
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the "
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
}
@@ -92,7 +92,7 @@ namespace MobileGL::MG_State::GLState {
// in-range elements.
const glslang::TType* type = m_program->getUniform(index).getType();
if (type == nullptr || !type->isArray()) return -1;
if (static_cast<GLint>(element) >= GetActiveUniformArraySize(index)) return -1;
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
const Int location = base + (Int)element;
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
return location;
@@ -104,11 +104,30 @@ namespace MobileGL::MG_State::GLState {
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
}
// ---- GL index <-> glslang TProgram index translation ----
// The single relaxed parse enumerates artifacts GL must not see: every declared
// default-block uniform (even dead ones) as a member of the synthesized
// MGL_GLOBAL_UBO, and that block itself. DoReflection builds filtered GL-facing
// 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];
}
Int GlUniformIndexFromTProgram(Int tIndex) const {
if (tIndex < 0 || tIndex >= static_cast<Int>(m_tProgramUniformIndexToGl.size())) return -1;
return m_tProgramUniformIndexToGl[tIndex];
}
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(m_tProgramBlockIndexToGl.size())) return -1;
return m_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());
if (uniformIndex >= 0 && uniformIndex < m_activeUniformCount &&
if (uniformIndex >= 0 && uniformIndex < tProgramCount &&
m_program->getUniform(uniformIndex).name == name) {
return uniformIndex;
return GlUniformIndexFromTProgram(uniformIndex);
}
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
@@ -117,9 +136,9 @@ namespace MobileGL::MG_State::GLState {
if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]";
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
m_program->getUniform(suffixedIndex).name == suffixedName) {
return suffixedIndex;
return GlUniformIndexFromTProgram(suffixedIndex);
}
return -1;
}
@@ -127,8 +146,9 @@ 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());
if (baseIndex < 0 || baseIndex >= m_activeUniformCount) return -1;
return m_program->getUniform(baseIndex).name == baseName ? baseIndex : -1;
if (baseIndex < 0 || baseIndex >= tProgramCount) return -1;
return m_program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
: -1;
}
Bool IsValidUniformLocation(Int location) const {
@@ -136,7 +156,8 @@ namespace MobileGL::MG_State::GLState {
if (static_cast<SizeT>(location) >= m_uniformIndexInTProgram.size()) return false;
const Int uniformIndexInProgram = m_uniformIndexInTProgram[location];
return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd &&
uniformIndexInProgram >= 0 && uniformIndexInProgram < m_activeUniformCount;
uniformIndexInProgram >= 0 &&
uniformIndexInProgram < static_cast<Int>(m_tProgramUniformIndexToGl.size());
}
GLenum GetUniformType(Uint location) const {
@@ -145,16 +166,17 @@ namespace MobileGL::MG_State::GLState {
}
GLenum GetActiveUniformType(Uint index) const {
auto& uniform = m_program->getUniform(static_cast<Int>(index));
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
return uniform.glDefineType;
}
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
// 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.
GLint GetActiveUniformArraySize(Uint index) const {
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
// m_uniformIndexInTProgram stores).
GLint GetUniformArraySizeByTIndex(Int tIndex) const {
const auto& uniform = m_program->getUniform(tIndex);
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isSizedArray()) {
return type->getOuterArraySize();
@@ -162,15 +184,23 @@ namespace MobileGL::MG_State::GLState {
return uniform.size < 1 ? 1 : uniform.size;
}
Int GetActiveUniformBlockIndex(Uint index) const {
auto& uniform = m_program->getUniform(static_cast<Int>(index));
return uniform.index;
GLint GetActiveUniformArraySize(Uint index) const {
return GetUniformArraySizeByTIndex(TProgramUniformIndex(index));
}
// GL_UNIFORM_OFFSET: byte offset within the owning named block. glslang already reports -1
// for a default-block uniform, which is exactly the spec value there.
Int GetActiveUniformBlockIndex(Uint index) const {
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
return GlBlockIndexFromTProgram(uniform.index);
}
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
// 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 {
return m_program->getUniform(static_cast<Int>(index)).offset;
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
return uniform.offset;
}
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
@@ -182,8 +212,8 @@ 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(static_cast<Int>(index));
if (uniform.index < 0) return -1;
const auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isArray()) return 0;
if (type->isMatrix()) {
@@ -202,8 +232,8 @@ 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(static_cast<Int>(index));
if (uniform.index < 0) return 0;
const auto& uniform = m_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;
@@ -220,8 +250,8 @@ 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(static_cast<Int>(index));
if (uniform.index < 0) return -1;
const auto& uniform = m_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;
@@ -250,7 +280,7 @@ namespace MobileGL::MG_State::GLState {
}
const String& GetActiveUniformName(Uint index) const {
auto& uniform = m_program->getUniform(static_cast<Int>(index));
auto& uniform = m_program->getUniform(TProgramUniformIndex(index));
return uniform.name;
}
// Sentinel for a uniform location without global-UBO backing storage (should not
@@ -321,7 +351,19 @@ namespace MobileGL::MG_State::GLState {
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; }
const String& GetActiveAttribName(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).name; }
// 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).
static const String& NormalizeBuiltinPipeInputName(const String& name) {
static const String kGlVertexId = "gl_VertexID";
static const String kGlInstanceId = "gl_InstanceID";
if (name == "gl_VertexIndex") return kGlVertexId;
if (name == "gl_InstanceIndex") return kGlInstanceId;
return name;
}
const String& GetActiveAttribName(Uint index) const {
return NormalizeBuiltinPipeInputName(m_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()); }
@@ -403,7 +445,9 @@ namespace MobileGL::MG_State::GLState {
Bool GetValidateStatus() const { return m_validateStatus; }
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
Int GetActiveUniformBlocksCount() const { return m_program->getNumUniformBlocks(); }
// 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; }
@@ -427,11 +471,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((Int)index).size + 15u) & ~15u;
return (m_program->getUniformBlock(m_glBlockIndexToTProgram[index]).size + 15u) & ~15u;
}
const String& GetUniformBlockName(Uint index) const {
auto& ubo = m_program->getUniformBlock((Int)index);
auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]);
return ubo.name;
}
@@ -462,7 +506,7 @@ namespace MobileGL::MG_State::GLState {
}
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
const auto& ubo = m_program->getUniformBlock((Int)index);
const auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]);
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
return (ubo.stages & stageMask) != 0;
}
@@ -546,13 +590,23 @@ namespace MobileGL::MG_State::GLState {
private:
void ResetLinkArtifacts();
void DoReflection();
// 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
// explicit-uniform-location conflict makes the link invalid.
Bool DoReflection();
// 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);
void GenerateBinary();
// The former GenerateBinary, split around DoReflection's data dependencies:
// SPIR-V must be generated BEFORE buildReflection touches m_program (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.
void GenerateSpirv();
void BuildGlobalUboRouting();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing();
Bool ValidateFragmentOutputLocations();
@@ -583,6 +637,18 @@ namespace MobileGL::MG_State::GLState {
Int m_maxFragmentOutputColorNumber = 8;
// 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`"
@@ -142,28 +142,33 @@ namespace {
namespace MobileGL::MG_State::GLState {
void ShaderObject::SetShaderSource(const String& source) {
m_source = source;
m_shader.reset();
m_compileStatus = false;
m_infoLog.clear();
InvalidateCompiledState();
}
void ShaderObject::SetShaderSource(String&& source) {
m_source = Move(source);
InvalidateCompiledState();
}
void ShaderObject::InvalidateCompiledState() {
m_shader.reset();
m_preprocessedSource.clear();
m_explicitUniformLocations.clear();
m_explicitOpaqueBindings.clear();
m_shaderConsumedByLink = false;
m_compileStatus = false;
m_infoLog.clear();
}
void ShaderObject::Compile() {
using namespace MG_Util::ShaderTranspiler;
InvalidateCompiledState();
String compileSource = m_source;
MG_Util::ShaderTranspiler::PreprocessShaderSource(m_stage, compileSource);
if (m_stage == ShaderStage::Compute) {
const std::optional<String> localSizeError = ValidateComputeLocalSizeLimits(compileSource);
if (localSizeError) {
m_compileStatus = false;
m_shader.reset();
m_infoLog = *localSizeError;
return;
}
@@ -172,27 +177,32 @@ namespace MobileGL::MG_State::GLState {
const std::optional<String> reservedError =
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
if (reservedError) {
m_compileStatus = false;
m_shader.reset();
m_infoLog = *reservedError;
return;
}
// Compile for OpenGL here, so that we can do validation and link
// like a real OpenGL driver at linking stage
// Will compile for other backends later.
// Single parse, in the link-compatible configuration (Vulkan-client env with
// relaxed rules): the TShader stored here is what glLinkProgram links and what
// the backends' SPIR-V is generated from - there is no second, GL-client parse
// anymore. The GL frontend semantics the relaxed parse cannot provide are
// restored on top: explicit default-block uniform locations through the lexical
// side-channel below, dead-uniform/global-UBO filtering in
// ProgramObject::DoReflection.
m_explicitUniformLocations = ExtractExplicitUniformLocations(compileSource);
m_explicitOpaqueBindings = ExtractExplicitOpaqueBindings(compileSource);
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
.sourceStr = compileSource,
.flags = ShaderCompileBits::CompileForOpenGL};
.flags = 0};
auto result = ShaderCompiler::CompileShader(attrib);
if (result) {
m_compileStatus = true;
m_shader = result.value();
m_preprocessedSource = Move(compileSource);
m_infoLog.clear();
} else {
m_compileStatus = false;
m_shader.reset();
m_explicitUniformLocations.clear();
m_explicitOpaqueBindings.clear();
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.",
@@ -200,6 +210,31 @@ namespace MobileGL::MG_State::GLState {
}
}
SharedPtr<glslang::TShader> ShaderObject::TakeShaderForLink(String& outReparseLog) {
if (m_shader && !m_shaderConsumedByLink) {
m_shaderConsumedByLink = true;
return m_shader;
}
// The stored parse already fed a link, whose mapIO mutated its intermediate.
// Re-parse the preprocessed source through the identical configuration; this
// costs one glslang parse, which is exactly what GenerateBinary used to spend
// 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};
auto result = ShaderCompiler::CompileShader(attrib);
if (!result) {
// Should be unreachable: the same source parsed successfully at Compile().
outReparseLog = result.error().log;
MGLOG_E("ShaderObject::TakeShaderForLink: re-parse of shader %d failed:\n%s", m_externalIndex,
outReparseLog.c_str());
return nullptr;
}
return result.value();
}
void ShaderObject::MarkAsDeleted() {
m_deleteStatus = true;
}
@@ -31,21 +31,49 @@ namespace MobileGL {
void Compile();
void MarkAsDeleted();
// Hands out a link-consumable TShader. glslang's mapIO mutates the TShader's
// aliased intermediate, so the parse stored by Compile() may feed exactly one
// link; every later link (relink, or the same shader attached to a second
// program) gets a fresh parse of the stored preprocessed source through the
// byte-identical CompileShader path (including the legacy-460 retry). Only
// callable while GetCompileStatus() is true. Returns null only if that
// re-parse fails - outReparseLog then carries its diagnostics.
SharedPtr<glslang::TShader> TakeShaderForLink(String& outReparseLog);
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; }
// 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;
}
// 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; }
Bool GetDeleteStatus() const { return m_deleteStatus; }
private:
void InvalidateCompiledState();
const Uint m_externalIndex = 0;
const ShaderStage m_stage;
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;
String m_infoLog;
Bool m_deleteStatus = false;
+255
View File
@@ -2365,3 +2365,258 @@ void main() { o_color = vec4(1.0); }
EXPECT_EQ(IsShader(fs), GL_FALSE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---- P0a single-parse regression tests ----
// glCompileShader now performs the one link-compatible (relaxed Vulkan-rules) parse;
// these pin the GL frontend semantics that parse cannot provide by itself.
namespace {
GLuint CompileShaderChecked(GLenum type, const char* source) {
char infoLog[1024] = "";
GLuint shader = CreateShader(type);
ShaderSource(shader, 1, &source, nullptr);
CompileShader(shader);
GLint status = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &status);
GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(status, GL_TRUE) << infoLog;
return shader;
}
GLuint LinkVsFs(GLuint vs, GLuint fs, GLint expectedLinkStatus) {
char infoLog[2048] = "";
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(linkStatus, expectedLinkStatus) << infoLog;
return program;
}
} // namespace
// The relaxed parse sweeps every DECLARED default-block uniform into MGL_GLOBAL_UBO,
// including ones no stage reads. GL requires those to be inactive: absent from the
// glGetActiveUniform enumeration and -1 from glGetUniformLocation. The synthesized
// MGL_GLOBAL_UBO itself must not surface as a GL uniform block either.
TEST_F(ProgramTest, DeclaredButUnreadUniformIsInactiveAndGlobalUboStaysHidden) {
const char* vsSource = R"(#version 330 core
uniform mat4 uUsedMat;
uniform vec4 uDeadVec;
void main() { gl_Position = uUsedMat * vec4(1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform vec4 uUsedColor;
uniform float uDeadFloat;
out vec4 fragColor;
void main() { fragColor = uUsedColor; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms);
EXPECT_EQ(activeUniforms, 2);
EXPECT_NE(GetUniformLocation(program, "uUsedMat"), -1);
EXPECT_NE(GetUniformLocation(program, "uUsedColor"), -1);
EXPECT_EQ(GetUniformLocation(program, "uDeadVec"), -1);
EXPECT_EQ(GetUniformLocation(program, "uDeadFloat"), -1);
EXPECT_EQ(UniformIndexByName(program, "uDeadVec"), GL_INVALID_INDEX);
char nameBuf[64] = "";
for (GLint i = 0; i < activeUniforms; ++i) {
GLsizei nameLen = 0;
GLint size = 0;
GLenum type = 0;
GetActiveUniform(program, static_cast<GLuint>(i), sizeof(nameBuf), &nameLen, &size, &type, nameBuf);
EXPECT_TRUE(std::strcmp(nameBuf, "uDeadVec") != 0 && std::strcmp(nameBuf, "uDeadFloat") != 0)
<< nameBuf;
}
// No named blocks are declared, so GL must see zero uniform blocks - the global
// UBO the transpiler materializes is an implementation artifact.
GLint activeBlocks = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &activeBlocks);
EXPECT_EQ(activeBlocks, 0);
EXPECT_EQ(GetUniformBlockIndex(program, "MGL_GLOBAL_UBO"), GL_INVALID_INDEX);
// Default-block uniforms report block index -1 and offset -1 even though the
// relaxed parse physically placed them in the global UBO.
const GLuint usedMat = UniformIndexByName(program, "uUsedMat");
ASSERT_NE(usedMat, GL_INVALID_INDEX);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_OFFSET), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_ARRAY_STRIDE), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_MATRIX_STRIDE), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_IS_ROW_MAJOR), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Distinct uniforms whose explicit locations overlap across stages must fail the
// link (ARB_explicit_uniform_location). The GL-client parse used to reject this at
// glslang mapIO; the relaxed parse drops the qualifiers, so the location assigner
// enforces it - this is the experiment's synthetic divergence case.
TEST_F(ProgramTest, ExplicitUniformLocationOverlapAcrossStagesFailsLink) {
const char* vsSource = R"(#version 460 core
layout(location = 3) uniform vec4 uVec[4];
void main() { gl_Position = uVec[0] + uVec[3]; }
)";
const char* fsSource = R"(#version 460 core
layout(location = 5) uniform float uF;
out vec4 fragColor;
void main() { fragColor = vec4(uF); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_FALSE);
char infoLog[1024] = "";
GLsizei logLength = 0;
GetProgramInfoLog(program, sizeof(infoLog), &logLength, infoLog);
EXPECT_GT(logLength, 0);
}
// The same uniform declared with different explicit locations in two stages is a
// link error as well.
TEST_F(ProgramTest, ConflictingExplicitUniformLocationsOnSameUniformFailLink) {
const char* vsSource = R"(#version 460 core
layout(location = 2) uniform vec4 uShared;
void main() { gl_Position = uShared; }
)";
const char* fsSource = R"(#version 460 core
layout(location = 4) uniform vec4 uShared;
out vec4 fragColor;
void main() { fragColor = uShared; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
(void)LinkVsFs(vs, fs, GL_FALSE);
}
// Same-location explicit declarations of the SAME uniform in both stages stay
// linkable, and both explicit locations (opaque and non-opaque) are honored.
TEST_F(ProgramTest, ExplicitUniformLocationsHonoredForPlainAndOpaqueUniforms) {
const char* vsSource = R"(#version 460 core
layout(location = 11) uniform mat4 uMvp;
void main() { gl_Position = uMvp * vec4(1.0); }
)";
const char* fsSource = R"(#version 460 core
layout(location = 7) uniform sampler2D uTex;
layout(location = 11) uniform mat4 uMvp;
out vec4 fragColor;
void main() { fragColor = texture(uTex, uMvp[0].xy); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_EQ(GetUniformLocation(program, "uMvp"), 11);
EXPECT_EQ(GetUniformLocation(program, "uTex"), 7);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// A glslang-auto-assigned opaque location may collide with a source-explicit plain
// uniform location under the relaxed parse (glslang no longer sees the plain
// uniform's qualifier). The assigner must relocate the auto one, not fail the link.
TEST_F(ProgramTest, AutoOpaqueLocationCollidingWithExplicitPlainLocationRelocates) {
const char* vsSource = R"(#version 460 core
layout(location = 0) uniform mat4 uM;
void main() { gl_Position = uM * vec4(1.0); }
)";
const char* fsSource = R"(#version 460 core
uniform sampler2D uTex;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vec2(0.5)); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
const GLint mLoc = GetUniformLocation(program, "uM");
const GLint texLoc = GetUniformLocation(program, "uTex");
EXPECT_EQ(mLoc, 0);
ASSERT_NE(texLoc, -1);
EXPECT_NE(texLoc, mLoc);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Relinking a program and linking the same compiled shaders into a second program
// both re-consume the stored single parse (glslang mapIO mutates a linked TShader,
// so reuse goes through the consume-once re-parse path). Reflection must be intact
// every time, without any glCompileShader in between.
TEST_F(ProgramTest, RelinkAndSecondProgramReuseCompiledShaders) {
const char* vsSource = R"(#version 330 core
uniform mat4 uMvp;
in vec3 aPos;
void main() { gl_Position = uMvp * vec4(aPos, 1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform sampler2D uTex;
uniform vec4 uTint;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vec2(0.5)) * uTint; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program1 = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms1 = 0;
GetProgramiv(program1, GL_ACTIVE_UNIFORMS, &activeUniforms1);
EXPECT_EQ(activeUniforms1, 3);
EXPECT_NE(GetUniformLocation(program1, "uMvp"), -1);
// Relink: consumes the re-parse path.
LinkProgram(program1);
GLint relinkStatus = GL_FALSE;
char infoLog[1024] = "";
GetProgramiv(program1, GL_LINK_STATUS, &relinkStatus);
GetProgramInfoLog(program1, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(relinkStatus, GL_TRUE) << infoLog;
GLint activeUniformsRelink = 0;
GetProgramiv(program1, GL_ACTIVE_UNIFORMS, &activeUniformsRelink);
EXPECT_EQ(activeUniformsRelink, 3);
EXPECT_NE(GetUniformLocation(program1, "uTint"), -1);
// Same shaders into a fresh program.
GLuint program2 = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms2 = 0;
GetProgramiv(program2, GL_ACTIVE_UNIFORMS, &activeUniforms2);
EXPECT_EQ(activeUniforms2, 3);
EXPECT_NE(GetUniformLocation(program2, "uTex"), -1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Programs and shaders share one GL name space (GL 3.3 core 2.11). A name must
// never be handed out as both, and a shader name passed where a program is
// expected is INVALID_OPERATION (KHR-GL30.get_uniform_tests.get_uniform relies
// on this; a name-collided linked program used to swallow the error).
TEST_F(ProgramTest, ProgramAndShaderNamesShareOneNameSpace) {
GLuint program = CreateProgram();
GLuint vs = CreateShader(GL_VERTEX_SHADER);
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
EXPECT_NE(program, vs);
EXPECT_NE(program, fs);
EXPECT_NE(vs, fs);
EXPECT_EQ(IsProgram(vs), GL_FALSE);
EXPECT_EQ(IsShader(program), GL_FALSE);
GLfloat floatValue = 0.0f;
GetUniformfv(vs, 0, &floatValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
GLint intValue = 0;
GetUniformiv(fs, 0, &intValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
// A never-allocated name is INVALID_VALUE, distinguishing the two cases.
GetUniformfv(program + vs + fs + 100, 0, &floatValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
DeleteShader(vs);
DeleteShader(fs);
DeleteProgram(program);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
@@ -10,6 +10,8 @@
#include <algorithm>
#include <cctype>
#include <climits>
#include <cstdlib>
#include <initializer_list>
#include <utility>
#include <Config.h>
@@ -1441,6 +1443,283 @@ namespace MobileGL {
return std::nullopt;
}
namespace {
bool IsNonLayoutQualifierKeyword(const String& text) {
static const char* kQualifiers[] = {
"highp", "mediump", "lowp", "precise", "const", "flat",
"noperspective", "smooth", "centroid", "sample", "patch", "invariant",
"coherent", "volatile", "restrict", "readonly", "writeonly", "subroutine",
};
for (const char* qualifier : kQualifiers) {
if (text == qualifier) return true;
}
return false;
}
bool IsDecimalIntegerToken(const String& text) {
if (text.empty()) return false;
return std::all_of(text.begin(), text.end(),
[](char ch) { return ch >= '0' && ch <= '9'; });
}
// Parses one brace-free depth-0 statement [begin, end) and records its
// declarators when it is a uniform declaration carrying an integral
// layout(location = N). Multi-declarator statements assign consecutive
// locations, each declarator advancing by its array element count
// (ARB_explicit_uniform_location rules). Anything the narrow grammar does
// not recognize is skipped, never guessed at.
void RecordUniformDeclarationLocations(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
using MobileGL::Int;
long long location = -1;
bool sawUniform = false;
SizeT declaratorBegin = end;
for (SizeT k = begin; k < end;) {
const String& text = tokens[k].text;
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
SizeT j = k + 2;
Int parenDepth = 1;
while (j < end && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
k = j;
continue;
}
if (text == "uniform") {
sawUniform = true;
++k;
continue;
}
if (sawUniform && location >= 0 && IsIdentifierToken(tokens[k]) &&
!IsNonLayoutQualifierKeyword(text)) {
declaratorBegin = k + 1; // 'text' is the type; declarators follow
break;
}
++k;
}
if (!sawUniform || location < 0 || declaratorBegin >= end) return;
long long nextLocation = location;
for (SizeT k = declaratorBegin; k < end;) {
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
const String& name = tokens[k].text;
++k;
long long span = 1;
while (k < end && tokens[k].text == "[") {
++k;
long long dimension = 1;
if (k < end && IsDecimalIntegerToken(tokens[k].text)) {
dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10);
++k;
}
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
span *= std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2)));
}
// Keep the first sighting: a duplicate can only come from alternative
// preprocessor branches declaring the same name.
locations.emplace(name, static_cast<Int>(std::min(
nextLocation, static_cast<long long>(INT_MAX / 2))));
nextLocation += span;
if (k >= end) break;
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
Int nestingDepth = 0;
++k;
while (k < end) {
const String& initializerToken = tokens[k].text;
if (initializerToken == "(" || initializerToken == "[") {
++nestingDepth;
} else if (initializerToken == ")" || initializerToken == "]") {
--nestingDepth;
} else if (initializerToken == "," && nestingDepth == 0) {
break;
}
++k;
}
}
if (k >= end) break;
if (tokens[k].text != ",") return;
++k;
}
}
// Parses one brace-free depth-0 statement [begin, end) and records its
// declarators when it is a sampler/image uniform declaration carrying an
// integral layout(binding = N). Such a binding is a GL texture/image unit,
// which the Vulkan-client relaxed parse strips before mapIO can observe it
// (it is not a valid descriptor binding there), so it is extracted lexically
// and restored as the uniform's initial unit. Every declarator in the
// statement shares the qualifier's binding, matching what the GL-client
// mapIO used to capture from the shared type qualifier. Anything the narrow
// grammar does not recognize is skipped, never guessed at.
void RecordOpaqueDeclarationBindings(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
using MobileGL::Int;
long long binding = -1;
bool sawUniform = false;
SizeT declaratorBegin = end;
for (SizeT k = begin; k < end;) {
const String& text = tokens[k].text;
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
SizeT j = k + 2;
Int parenDepth = 1;
while (j < end && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
k = j;
continue;
}
if (text == "uniform") {
sawUniform = true;
++k;
continue;
}
if (sawUniform && binding >= 0 && IsIdentifierToken(tokens[k]) &&
!IsNonLayoutQualifierKeyword(text)) {
// 'text' is the type. Only sampler/image opaques carry unit
// bindings; on anything else (e.g. atomic_uint, whose binding
// is a counter-buffer index) record nothing.
if (text.find("sampler") == String::npos && text.find("image") == String::npos) return;
declaratorBegin = k + 1;
break;
}
++k;
}
if (!sawUniform || binding < 0 || declaratorBegin >= end) return;
for (SizeT k = declaratorBegin; k < end;) {
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
const String& name = tokens[k].text;
++k;
while (k < end && tokens[k].text == "[") {
++k;
if (k < end && IsDecimalIntegerToken(tokens[k].text)) ++k;
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
}
bindings[name] = static_cast<MobileGL::Uint>(binding);
if (k >= end) break;
if (tokens[k].text != ",") return; // opaque declarators cannot take initializers
++k;
}
}
} // namespace
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source) {
UnorderedMap<String, Uint> bindings;
// Fast path: without the qualifier keyword there is nothing to extract.
if (source.find("binding") == String::npos) return bindings;
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
Int braceDepth = 0;
SizeT pos = 0;
while (pos < count) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
++pos;
continue;
}
if (text == "}") {
if (braceDepth > 0) --braceDepth;
++pos;
continue;
}
if (braceDepth != 0 || text == ";") {
++pos;
continue;
}
// A depth-0 statement runs to its ';'. One that opens a brace instead is
// a function definition or an interface/uniform block: a block's binding
// is a buffer binding point, not a texture unit, so skip both alike.
SizeT statementEnd = pos;
while (statementEnd < count && tokens[statementEnd].text != ";" &&
tokens[statementEnd].text != "{") {
++statementEnd;
}
if (statementEnd >= count || tokens[statementEnd].text == "{") {
pos = statementEnd;
continue;
}
RecordOpaqueDeclarationBindings(tokens, pos, statementEnd, bindings);
pos = statementEnd + 1;
}
return bindings;
}
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
UnorderedMap<String, Int> locations;
// Fast path: without the qualifier keyword there is nothing to extract.
if (source.find("location") == String::npos) return locations;
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
Int braceDepth = 0;
SizeT pos = 0;
while (pos < count) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
++pos;
continue;
}
if (text == "}") {
if (braceDepth > 0) --braceDepth;
++pos;
continue;
}
if (braceDepth != 0 || text == ";") {
++pos;
continue;
}
// A depth-0 statement runs to its ';'. One that opens a brace instead is a
// function definition or an interface/uniform block: neither can declare a
// default-block uniform location, so hand the '{' back to the depth tracker.
SizeT statementEnd = pos;
while (statementEnd < count && tokens[statementEnd].text != ";" &&
tokens[statementEnd].text != "{") {
++statementEnd;
}
if (statementEnd >= count || tokens[statementEnd].text == "{") {
pos = statementEnd;
continue;
}
RecordUniformDeclarationLocations(tokens, pos, statementEnd, locations);
pos = statementEnd + 1;
}
return locations;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -45,6 +45,28 @@ namespace MobileGL {
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the
// compile-error text for the first violation, or nullopt for a clean source.
std::optional<String> FindReservedIdentifierViolation(const String& source);
// Explicit layout(location = N) qualifiers on default-block uniform declarations,
// keyed by declared name (no "[0]" suffix). Multi-declarator statements assign
// consecutive locations, advancing by the array element count.
//
// Exists because the single link-compatible parse runs under relaxed Vulkan rules,
// where glslang's vkRelaxedRemapUniformVariable moves plain uniforms into
// MGL_GLOBAL_UBO and DISCARDS their location qualifiers ("ignoring layout qualifier
// for uniform location"); opaque uniforms keep theirs. This lexical side-channel
// restores the discarded locations to the GL location assigner
// (ProgramObject::DoReflection). It scans preprocessor-visible text, so a
// declaration inside an inactive #if branch is still recorded - harmless unless a
// pack declares the same uniform with different explicit locations in alternative
// branches (none observed; explicit uniform locations have zero incidence in the
// shader-pack corpus, this is an ARB_explicit_uniform_location conformance surface).
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source);
// Explicit layout(binding = N) on sampler/image uniforms, i.e. their initial
// texture/image units. The Vulkan-client relaxed parse strips these before
// mapIO can capture them, so they are recovered lexically (same narrow
// grammar discipline as ExtractExplicitUniformLocations).
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL