mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user