diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 7e9f66ab..acff6e6f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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(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(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(m_glUniformIndexToTProgram.size()); + m_glUniformIndexToTProgram.push_back(i); + } + m_activeUniformCount = static_cast(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 effectiveLocation(tProgramUniformCount, kNoLocation); + Vector locationIsSourceExplicit(tProgramUniformCount, false); + UnorderedMap 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(*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(rootIt->second)); + (void)inserted; + effectiveLocation[i] = cursor->second; + locationIsSourceExplicit[i] = true; + cursor->second += static_cast(GetUniformLocationSpan(uniform)); + } + } + if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) { + effectiveLocation[i] = uniform.layoutLocation(); + } + if (locationIsSourceExplicit[i] && + effectiveLocation[i] + static_cast(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 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> 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 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 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(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(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(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); } diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index c19b8c69..565fc670 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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(element) >= GetActiveUniformArraySize(index)) return -1; + if (static_cast(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(m_tProgramUniformIndexToGl.size())) return -1; + return m_tProgramUniformIndexToGl[tIndex]; + } + Int GlBlockIndexFromTProgram(Int tBlockIndex) const { + if (tBlockIndex < 0 || tBlockIndex >= static_cast(m_tProgramBlockIndexToGl.size())) return -1; + return m_tProgramBlockIndexToGl[tBlockIndex]; + } + Int GetActiveUniformIndex(const String& name) const { + const Int tProgramCount = static_cast(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(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(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(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(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(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(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(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(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(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(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(index)).glDefineType; } GLint GetActiveAttribArraySize(Uint index) const { return m_program->getPipeInput(static_cast(index)).size; } - const String& GetActiveAttribName(Uint index) const { return m_program->getPipeInput(static_cast(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(index)).name); + } void* MapUBO() { return m_globalUboScratch.data(); } const void* GetUBOData() const { return m_globalUboScratch.data(); } Uint GetUBOSize() const { return static_cast(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(m_glBlockIndexToTProgram.size()); } GLuint GetComputeLocalSize(Uint dim) const { return m_program->getLocalSize(static_cast(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(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 m_glUniformIndexToTProgram; + Vector m_tProgramUniformIndexToGl; + Vector m_glBlockIndexToTProgram; + Vector 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 m_linkedExplicitUniformLocations; UnorderedMap m_uniformLocations; // Ordered by location, // aka. m_uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp index f476ae98..2fc1efff 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp @@ -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 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 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 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; } diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h index c4a93e62..74ac9ebb 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h @@ -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 TakeShaderForLink(String& outReparseLog); + Uint GetExternalIndex() const { return m_externalIndex; } ShaderStage GetShaderStage() const { return m_stage; } const String& GetShaderSource() const { return m_source; } const SharedPtr& GetCompiledShader() const { return m_shader; } const String& GetInfoLog() const { return m_infoLog; } const UnorderedMap& 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& 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& 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 m_shader; UnorderedMap m_uniforms; + UnorderedMap m_explicitUniformLocations; + UnorderedMap m_explicitOpaqueBindings; + Bool m_shaderConsumedByLink = false; String m_infoLog; Bool m_deleteStatus = false; diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index ab8d175d..f4541f85 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -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(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(GL_INVALID_OPERATION)); + GLint intValue = 0; + GetUniformiv(fs, 0, &intValue); + EXPECT_EQ(GetError(), static_cast(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(GL_INVALID_VALUE)); + + DeleteShader(vs); + DeleteShader(fs); + DeleteProgram(program); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index b0cb84a7..6e45cd86 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -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& tokens, SizeT begin, SizeT end, + MobileGL::UnorderedMap& 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(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(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(std::min( + nextLocation, static_cast(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& tokens, SizeT begin, SizeT end, + MobileGL::UnorderedMap& 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(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(binding); + if (k >= end) break; + if (tokens[k].text != ",") return; // opaque declarators cannot take initializers + ++k; + } + } + } // namespace + + UnorderedMap ExtractExplicitOpaqueBindings(const String& source) { + UnorderedMap bindings; + // Fast path: without the qualifier keyword there is nothing to extract. + if (source.find("binding") == String::npos) return bindings; + + const Vector 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 ExtractExplicitUniformLocations(const String& source) { + UnorderedMap locations; + // Fast path: without the qualifier keyword there is nothing to extract. + if (source.find("location") == String::npos) return locations; + + const Vector 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h index d74fcde8..cd340254 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h @@ -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 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 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 ExtractExplicitOpaqueBindings(const String& source); } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL