mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
234 lines
13 KiB
C++
234 lines
13 KiB
C++
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
//
|
|
// Created by Swung 0x48 on 2025/11/10.
|
|
//
|
|
|
|
#include "TMglGlslIoResolver.h"
|
|
|
|
#include <cstring>
|
|
|
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
|
|
namespace MobileGL {
|
|
bool TMglGlslIoResolver::ShouldAssignPlainUniformLocation(const glslang::TType& type) const {
|
|
if (!doAutoLocationMapping()) {
|
|
return false;
|
|
}
|
|
|
|
if (type.getQualifier().hasLocation()) {
|
|
return false;
|
|
}
|
|
|
|
if (type.isBuiltIn() || type.getBasicType() == glslang::EbtBlock || type.isAtomic() || type.isSpirvType() ||
|
|
(type.containsOpaque() && referenceIntermediate.getSpv().openGl == 0)) {
|
|
return false;
|
|
}
|
|
|
|
if (type.isStruct()) {
|
|
if (type.getStruct()->size() < 1) {
|
|
return false;
|
|
}
|
|
if ((*type.getStruct())[0].type->isBuiltIn()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void TMglGlslIoResolver::EnsurePlainUniformLocationsAssigned() {
|
|
if (m_plainUniformLocationsAssigned) {
|
|
return;
|
|
}
|
|
m_plainUniformLocationsAssigned = true;
|
|
|
|
const int resourceKey = buildStorageKey(EShLangCount, glslang::EvqUniform);
|
|
auto& slotMap = storageSlotMap[resourceKey];
|
|
for (const auto& [name, size] : m_plainUniformLocationSizeByName) {
|
|
const auto existingLocation = slotMap.find(name);
|
|
if (existingLocation != slotMap.end()) {
|
|
m_plainUniformLocationByName[name] = existingLocation->second;
|
|
continue;
|
|
}
|
|
|
|
const int location = getFreeSlot(resourceKey, 0, size);
|
|
slotMap[name] = location;
|
|
m_plainUniformLocationByName[name] = location;
|
|
}
|
|
}
|
|
|
|
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
|
const glslang::TType& type = ent.symbol->getType();
|
|
const glslang::TString& name = ent.symbol->getAccessName();
|
|
// OpenGL assigns generic vertex attribute locations only to active inputs. glslang gathers
|
|
// both live and dead declarations before mapping, so allowing the default collector to
|
|
// reserve a dead vertex input would make it consume a location that an active input should
|
|
// reuse. Other stage interfaces still need the default cross-stage matching behavior.
|
|
if (!ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
|
|
return;
|
|
}
|
|
// glBindAttribLocation only affects active inputs in the linked program. Applying an API
|
|
// binding to an inactive declaration would reserve its slot in glslang's collector and
|
|
// incorrectly push an active, automatically mapped input to a different location.
|
|
if (ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
|
|
auto it = m_explicitVertexIns.find(name.c_str());
|
|
if (it != m_explicitVertexIns.end()) {
|
|
auto& writableType = ent.symbol->getWritableType();
|
|
writableType.getQualifier().layoutLocation = it->second;
|
|
}
|
|
}
|
|
if (currentStage == EShLangFragment && type.getQualifier().isPipeOutput()) {
|
|
auto it = m_explicitFragOuts.find(name.c_str());
|
|
if (it != m_explicitFragOuts.end()) {
|
|
auto& writableType = ent.symbol->getWritableType();
|
|
writableType.getQualifier().layoutLocation = it->second;
|
|
}
|
|
// Dual-source blend color index (glBindFragDataLocationIndexed) -> layout(index = N).
|
|
// Only the non-zero (dual-source) index is emitted: index 0 is the GL default, and
|
|
// emitting an explicit "index = 0" qualifier would demand GL_EXT_blend_func_extended on
|
|
// GLES even for ordinary single-source fragment outputs.
|
|
auto idxIt = m_explicitFragOutIndices.find(name.c_str());
|
|
if (idxIt != m_explicitFragOutIndices.end() && idxIt->second != 0) {
|
|
auto& writableType = ent.symbol->getWritableType();
|
|
writableType.getQualifier().layoutIndex = idxIt->second;
|
|
}
|
|
}
|
|
if (ShouldAssignPlainUniformLocation(type)) {
|
|
const int size = glslang::TIntermediate::computeTypeUniformLocationSize(type);
|
|
auto& recordedSize = m_plainUniformLocationSizeByName[name];
|
|
recordedSize = std::max(recordedSize, size);
|
|
}
|
|
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
|
|
}
|
|
|
|
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
|
// NO dead-vertex-input early-out here, deliberately - the skip belongs in
|
|
// reserverStorageSlot() and ONLY there.
|
|
//
|
|
// Skipping RESERVATION is the GL semantic: only active inputs get generic attribute
|
|
// locations, so a dead declaration must not consume a slot an active input should
|
|
// have. Skipping RESOLUTION as well used to look like the same statement, but it is a
|
|
// different one: it leaves the variable with no layoutLocation, and glslang still
|
|
// EMITS it - a declared input is in the shader's linker objects and therefore in the
|
|
// entry point's interface. The result is an OpVariable of storage class Input with no
|
|
// Location decoration, which SPIR-V forbids
|
|
// (VUID-StandaloneSpirv-Location-04916). lavapipe tolerates it; Adreno rejects the
|
|
// whole pipeline with VK_ERROR_UNKNOWN, which is how this shipped undetected - every
|
|
// desktop gate, retrace corpus included, is blind to it.
|
|
//
|
|
// Found 2026-08-11 on an Adreno 830: the Iris weather program (mc_midTexCoord among
|
|
// seven attributes, only some of them glBindAttribLocation-bound) died at the first
|
|
// rainy-world draw, 100% reproducible, programHash 0x4a7e9a37fb49caa1.
|
|
//
|
|
// They cannot simply be handed to the base resolver either. Auto-assignment for inputs
|
|
// WITHOUT an explicit binding happens entirely in the resolve pass, in sort order, so a
|
|
// dead declaration reaching the free-slot search first would take location 0 and push
|
|
// the active input up - which is precisely the GL violation the reservation skip
|
|
// exists to prevent (ProgramTest.InactiveExplicitVertexBindingsDoNotReserveLocations
|
|
// pins it: Iris injects Position/UV0 into packs that actually read vaPosition).
|
|
//
|
|
// So dead inputs get their locations from the TOP of the attribute range downward,
|
|
// while the base resolver hands active ones out from 0 upward. Both properties hold at
|
|
// once: every emitted input carries a Location, and no active input is displaced. The
|
|
// two allocators can only meet if live + dead exceed the attribute limit, which is an
|
|
// over-subscribed program GL would reject anyway; if that happens we leave the
|
|
// variable to the base resolver rather than hand out a colliding location.
|
|
const glslang::TType& type = ent.symbol->getType();
|
|
if (!ent.live && stage == EShLangVertex && type.getQualifier().isPipeInput() &&
|
|
!type.getQualifier().hasLocation() && !type.isBuiltIn()) {
|
|
const int size = std::max(1, glslang::TIntermediate::computeTypeLocationSize(type, stage));
|
|
if (m_nextInactiveVertexInLocation - (size - 1) >= 0) {
|
|
m_nextInactiveVertexInLocation -= (size - 1);
|
|
ent.symbol->getWritableType().getQualifier().layoutLocation = m_nextInactiveVertexInLocation;
|
|
--m_nextInactiveVertexInLocation;
|
|
}
|
|
}
|
|
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
|
}
|
|
|
|
// THE COLLECT CALLBACK IS THE CAPTURE POINT, and the reason is a matter of ten lines of
|
|
// glslang. mapIO gathers every declared symbol of every stage and calls this on each of
|
|
// them (iomapper.cpp addStage -> TSlotCollector) BEFORE it resolves anything; only
|
|
// afterwards, in doMap(), does it write the slots it chose back into the types
|
|
// (iomapper.cpp:240, `layoutBinding = at->second.newBinding`). Up to here
|
|
// `qualifier.hasBinding()` still answers "did the SHADER say so?"; past it, every resource
|
|
// carries a number and the question can no longer be asked at all.
|
|
//
|
|
// Both captures below used to be lexical scans of the shader source, which had to run
|
|
// before the preprocessor's macros were expanded and therefore could not read
|
|
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
|
|
// its storage blocks. Asking the AST instead makes the macro case ordinary.
|
|
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
|
const glslang::TType& type = ent.symbol->getType();
|
|
const glslang::TQualifier& qualifier = type.getQualifier();
|
|
// getAccessName() is the BLOCK TYPE name for a block and the declared name for
|
|
// everything else (IntermTraverse.cpp TIntermSymbol::getAccessName) - which is exactly
|
|
// the key both consumers want.
|
|
const glslang::TString& name = ent.symbol->getAccessName();
|
|
|
|
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
|
qualifier.hasBinding()) {
|
|
(*m_explicitOpaqueUniformBindings)[name.c_str()] = qualifier.layoutBinding;
|
|
}
|
|
|
|
// A storage block that declared no binding. UNION across stages by construction - one
|
|
// resolver serves the whole program - which is what GLSL's "every stage must declare
|
|
// the same block identically" rule makes correct.
|
|
//
|
|
// NOT the atomic-counter blocks glslang SYNTHESIZES, which are storage blocks by every
|
|
// structural test available here and are still not what this set means. Relaxed parsing
|
|
// folds each atomic_uint into a "gl_AtomicCounterBlock_<GL binding>" block
|
|
// (ParseContextBase::growAtomicCounterBlock) and leaves it unbound because MobileGL asks
|
|
// for auto-mapped bindings - so it arrives looking exactly like an unqualified
|
|
// application block. Seeding one to GL binding 0 would overwrite the counter buffer's
|
|
// real binding, which is the trailing number in that very name.
|
|
if (m_storageBlocksWithoutBinding != nullptr && type.getBasicType() == glslang::EbtBlock &&
|
|
qualifier.storage == glslang::EvqBuffer && !qualifier.hasBinding() &&
|
|
name.compare(0, std::strlen(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX),
|
|
MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX) != 0) {
|
|
m_storageBlocksWithoutBinding->insert(name.c_str());
|
|
}
|
|
|
|
// A UNIFORM block that declared no binding. Same capture point and same union-across-
|
|
// stages reasoning as the storage-block set above, and the same reason it cannot be
|
|
// asked later: mapIO is about to write an auto-assigned binding into this very
|
|
// qualifier. MGL_GLOBAL_UBO is MobileGL's own synthesized block, not an application
|
|
// one - it never reaches the GL block space and must not be seeded here.
|
|
if (m_uniformBlocksWithoutBinding != nullptr && type.getBasicType() == glslang::EbtBlock &&
|
|
qualifier.storage == glslang::EvqUniform && !qualifier.hasBinding() &&
|
|
name.compare(MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != 0) {
|
|
m_uniformBlocksWithoutBinding->insert(name.c_str());
|
|
}
|
|
|
|
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
|
}
|
|
|
|
int TMglGlslIoResolver::resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
|
const glslang::TType& type = ent.symbol->getType();
|
|
if (type.getQualifier().hasLocation()) {
|
|
return TDefaultGlslIoResolver::resolveUniformLocation(stage, ent);
|
|
}
|
|
|
|
if (!ShouldAssignPlainUniformLocation(type)) {
|
|
return TDefaultGlslIoResolver::resolveUniformLocation(stage, ent);
|
|
}
|
|
|
|
EnsurePlainUniformLocationsAssigned();
|
|
|
|
const glslang::TString& name = ent.symbol->getAccessName();
|
|
const auto location = m_plainUniformLocationByName.find(name);
|
|
if (location == m_plainUniformLocationByName.end()) {
|
|
return ent.newLocation = -1;
|
|
}
|
|
|
|
return ent.newLocation = location->second;
|
|
}
|
|
} // namespace MobileGL
|