Files
MobileGL/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp
T

2842 lines
191 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersPrograms.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
#include "ManagersInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
// points. Those slots are taken from the TOP of the range downwards - below the one
// mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards
// never meets them, and the slot for GL binding N is `this - N` in every stage of the
// program without any shared state. Negative when the driver has no room left at all.
static Int AtomicCounterEsslBindingTop() {
return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2;
}
static Bool ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() {
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS
// feature toggle, both resolved in FillInGLESCapabilities.
return g_GLESCapabilities.AvoidExplicitLodBias;
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL
// advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl
// AND is part of the L2 translation-memo key, so the transpile and the key must read
// the same constant - hence one definition rather than two locals.
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is
// backend code and must not reach into the GL frontend. 4 is that translation unit's
// kFrontendMaxSamples, which is the source of truth - keep the two in step.
constexpr Int kFrontendMaxSamples = 4;
// Whether a usampler2DMS fetch's sample index is bounded at RUNTIME by the bound texture's
// real sample count rather than at compile time by the integer ceiling: armed when that
// ceiling is squeezed below what MobileGL advertises AND the depth ceiling - which the
// stencil aspect of a multisample depth/stencil texture is allocated under, and which a
// usampler reads too - exceeds it, so no one static bound is right for both. Read on both
// sides of the translation-memo boundary (the SPIR-V pass defers, the ESSL pass takes
// over); it is a function of three values the memo key already carries, so the two halves
// cannot disagree about one program.
static Bool ClampsUnsignedMultisampleFetchesAtRuntime() {
const Int advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
return g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples &&
g_GLESCapabilities.MaxDepthTextureSamples > g_GLESCapabilities.MaxIntegerSamples;
}
static Uint ResolveBackendEsslVersion() {
const auto& version = g_GLESCapabilities.GLESVersion;
if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) {
return 320;
}
if (version.Major == 3 && version.Minor >= 1) {
return 310;
}
return 300;
}
// The transpile pipeline invents image binding numbers: when the GL source declares
// an image uniform without layout(binding), glslang auto-assigns one (desktop GL
// allows that and lets the app pick the unit with glUniform1i, which ES forbids on
// image uniforms). The unit the app actually addresses lives in frontend state: the
// layout(binding) reflected at link time, or whatever glUniform1i stored afterwards.
// Rewrite every image uniform declaration to that unit so imageLoad/Store hits the
// unit the app bound with glBindImageTexture.
String RebindImageUniformsToFrontendUnits(String source, const PrgramImpl::ProgramBuildSource& src) {
// P5e (pg): the SOURCE, not the frontend object. The two reads below - the uniform's
// location and the unit glUniform1i put there - are exactly the pair the record's
// set_program_bindings tail carries, so on the handle arm this bakes the unit the client
// observed rather than one it would have had to dereference a live ProgramObject for.
// The null test the SharedPtr form carried moved to the caller, which returned early on
// a null program long before it reached here.
if (source.find("image") == String::npos) {
return source;
}
static const std::regex imageDeclRegex(
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
String result;
result.reserve(source.size());
SizeT lineStart = 0;
while (lineStart <= source.size()) {
const SizeT lineEnd = source.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = source.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
std::smatch match;
if (std::regex_search(line, match, imageDeclRegex)) {
const String name = match[3].str();
Int location = src.GetUniformLocation(name);
if (location < 0) {
location = src.GetUniformLocation(name + "[0]");
}
if (location >= 0) {
const Int unit = src.GetUniformSamplerOrImageUnitIndex(location);
if (unit >= 0) {
const String bindingText = "binding = " + std::to_string(unit);
if (std::regex_search(line, bindingValueRegex)) {
line = std::regex_replace(line, bindingValueRegex, bindingText);
} else if (match[1].matched) {
const SizeT layoutOpen = line.find('(', match.position(1));
line.insert(layoutOpen + 1, bindingText + ", ");
} else {
line.insert(match.position(0), "layout(" + bindingText + ") ");
}
}
}
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) {
// The composite band is a CLIENT-side indexing detail (contract D13) and the server
// must not learn about it: one reader, one answer, and the band's own table is indexed
// by (slot - base) exactly as the allocator's is.
if (MG_Pipe::MGPipeIsCompositeShaderSlot(cso.Slot)) {
const auto& band = MG_Pipe::MGPipeApplier().CompositeShaderCsos;
const Uint32 index = cso.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
if (index >= band.size()) return nullptr;
const auto& record = band[index];
if (!record.Live || record.Gen != cso.Gen) return nullptr;
return &record;
}
return PipeRecordAt(MG_Pipe::MGPipeApplier().ShaderCsos, cso);
}
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0;
// Every error-queue drain in the program build path is bounded by this: a lost
// context never answers GL_NO_ERROR, and the build runs on the thread that would
// then spin forever.
constexpr Int kMaxDrainedProgramErrors = 32;
TwinRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl, MG_Pipe::MGPipeKind::ShaderCso>
g_backendProgramObjects;
// ---- P5e (pg): the source, both ways round ---------------------------------------
Int ProgramArchiveSource::GetUniformLocation(const String& name) const {
// ProgramObject::GetUniformLocation over the archive. Reproduced rather than called
// because the frontend's form is a non-static member over Artifacts() and this arm
// has no ProgramObject to call it on; the ARRAY RULES are the whole body and they
// are the part a paraphrase would get wrong, so they are transcribed exactly.
const auto it = Link->uniformLocations.find(name);
if (it != Link->uniformLocations.end()) return static_cast<Int>(it->second);
if (name.empty()) return -1;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" at its base
// location, so a bare "arr" resolves to that entry.
if (name.back() != ']') {
const auto suffixed = Link->uniformLocations.find(name + "[0]");
if (suffixed != Link->uniformLocations.end()) return static_cast<Int>(suffixed->second);
return -1;
}
if (name.length() < 4) return -1;
// An ARRAY OF ARRAYS is keyed by its full "[0]"-terminated spelling ("a[2][1][0]"),
// so a query that already ends in a subscript may still be the NAME of an array
// rather than an element of one. That reading is tried FIRST; only then is the
// trailing subscript read as an element index.
{
const auto arrayOfArrays = Link->uniformLocations.find(name + "[0]");
if (arrayOfArrays != Link->uniformLocations.end()) {
return static_cast<Int>(arrayOfArrays->second);
}
}
const SizeT bracket = name.rfind('[');
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1;
}
auto base = Link->uniformLocations.find(name.substr(0, bracket) + "[0]");
if (base == Link->uniformLocations.end()) {
base = Link->uniformLocations.find(name.substr(0, bracket));
if (base == Link->uniformLocations.end()) return -1;
}
const Int baseLocation = static_cast<Int>(base->second);
if (!IsValidUniformLocation(baseLocation)) return -1;
const Int index = Link->uniformIndexInTProgram[baseLocation];
if (!UniformAt(index).type.isArray) return -1;
if (static_cast<GLint>(element) >=
MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) {
return -1;
}
const Int location = baseLocation + static_cast<Int>(element);
if (!UniformLocationsAliasSameUniform(baseLocation, location)) return -1;
return location;
}
const String& ProgramArchiveSource::GetUniformBlockName(Uint index) const {
static const String kEmpty;
if (index >= Link->glBlockIndexToTProgram.size()) return kEmpty;
const Int tBlockIndex = Link->glBlockIndexToTProgram[index];
if (tBlockIndex < 0 || static_cast<SizeT>(tBlockIndex) >= Link->blockReflection.size()) {
return kEmpty;
}
return Link->blockReflection[tBlockIndex].name;
}
Uint ProgramArchiveSource::GetUniformBlockBinding(Uint index) const {
// THE OVERLAY WINS WHEN THERE IS ONE, and the archive's link-time value stands when
// there is not. glUniformBlockBinding moves this AFTER the link that produced the
// archive, so a twin answering from the archive alone would bind the program's
// blocks to the points it was LINKED with rather than the ones it is BOUND with.
if (OverlayGoverns && BlockBindingOverlay != nullptr) {
if (index < BlockBindingOverlay->size()) {
return static_cast<Uint>((*BlockBindingOverlay)[index]);
}
// A block past the tail the record declared. The tail is dense over
// GetActiveUniformBlocksCount(), so this is a record and an archive that
// disagree about how many blocks the program has - which can only happen if a
// create and a bindings record crossed. 0 is GL's own default and is the safe
// answer; the mismatch is the emitter's to fix, not this read's to hide.
return 0;
}
if (index >= Link->uniformBlockBinding.size()) return 0;
return static_cast<Uint>(Link->uniformBlockBinding[index]);
}
Int ProgramArchiveSource::GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (OverlayGoverns) {
// THE TAIL IS THE WHOLE SET, so a location it does not name has no unit - NOT
// whatever the archive's link-time snapshot said. The client emits every
// assigned unit it can see, including the ones the link seeded from
// layout(binding=N), so "absent" really does mean -1.
//
// Sparse and ASCENDING BY LOCATION on the wire (CONTRACT-P5E §1), which is what
// lets this be a binary search: the build walks every location from 0 to
// maxUniformLocation, and a linear scan per location would be quadratic on a
// program with a thousand locations and a handful of samplers.
if (SamplerUnitOverlay == nullptr) return -1;
const auto it = std::lower_bound(
SamplerUnitOverlay->begin(), SamplerUnitOverlay->end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { return entry.Location < value; });
if (it == SamplerUnitOverlay->end() || it->Location != location) return -1;
return static_cast<Int>(it->Unit);
}
if (location >= Link->uniformSamplerOrImageUnitIndex.size()) return -1;
return Link->uniformSamplerOrImageUnitIndex[location];
}
ProgramArchiveSource ProgramArchiveSource::FromRecord(MG_Pipe::MGPipeHandle cso,
const MG_Pipe::MGPipeShaderCsoRecord& record) {
// THE HANDLE ARM. Every field below is server-owned: the archive the create adopted,
// the descriptor the create stored, and the three tails set_program_bindings
// carried. Nothing here touches a ProgramObject, which is the property the strict
// lane measures.
ProgramArchiveSource source;
source.Link = &record.Archive->Link;
source.Spirv = &record.Archive->Spirv;
source.Identity = cso.Slot;
source.IdentityIsHandleSlot = true;
// LinkStatus IS A FIELD NOW, and that is the whole point of c0e adding it: before
// P5e "linked" was only implied by the create existing, which is a different
// statement - create_shader_state is re-issued at every link that moves the link
// version, and a FAILED relink of a bound program moves it too (ID-88).
source.Linked = record.Desc.LinkStatus != 0;
source.SpirvUsable = record.Desc.SpirvStatus != 0;
source.SpirvValidationEnabled = record.Desc.EnableSpirvValidation != 0;
source.PointSizeWasDemoted = record.Desc.PointSizeDemoted != 0;
source.GlobalUboSize = record.Desc.GlobalUboSize;
source.LinkedStages.reserve(record.Archive->LinkedStages.size());
for (const Uint32 stage : record.Archive->LinkedStages) {
source.LinkedStages.push_back(static_cast<ShaderStage>(stage));
}
// "HAS A BINDINGS RECORD EVER BEEN APPLIED TO THIS RECORD" - not "is the tail
// non-empty". A program with no uniform blocks, no sampler and no override emits a
// record with three empty tails, and that record is the statement that the archive's
// link-time values are no longer the answer.
source.OverlayGoverns = record.BindingsSerial != 0;
source.BlockBindingOverlay = &record.BlockBindings;
source.SamplerUnitOverlay = &record.SamplerUnits;
if (source.OverlayGoverns) {
source.StorageOverrides.reserve(record.StorageOverrides.size());
for (const auto& entry : record.StorageOverrides) {
source.StorageOverrides[entry.Name] = entry.Binding;
}
} else {
source.StorageOverrides = record.Archive->Link.shaderStorageBlockBinding;
}
// THE RECORD'S NUMBER EITHER WAY, and deliberately not a recomputation when the
// overlay is absent. This is the value the twin STAMPS and the draw path's clean
// clause COMPARES (CONTRACT-P5E §5.5 names record.Signature as the input), so the
// two have to be the same field or every draw of a program with an override
// rebuilds. A record with no bindings applied carries 0 while the archive may hold
// a link-time override map - the next set_program_bindings then produces a non-zero
// signature and forces exactly one rebuild, which is correct and, since the emitter
// sends the bindings in the same breath as the create, not a state a draw reaches.
source.StorageOverrideSignature = record.Signature;
return source;
}
Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex) {
if (blockIndex < 0) return 0;
const SizeT index = static_cast<SizeT>(blockIndex);
if (record.BindingsSerial != 0) {
// The tail is the whole set; a block past it is a record and an archive that
// disagree about the block count, and GL's own default of 0 is the safe answer.
return index < record.BlockBindings.size() ? static_cast<Uint>(record.BlockBindings[index]) : 0;
}
if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0;
return static_cast<Uint>(record.Archive->Link.uniformBlockBinding[index]);
}
Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location) {
if (record.BindingsSerial != 0) {
const auto it = std::lower_bound(
record.SamplerUnits.begin(), record.SamplerUnits.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) { return entry.Location < value; });
if (it == record.SamplerUnits.end() || it->Location != location) return -1;
return static_cast<Int>(it->Unit);
}
if (!record.Archive || location >= record.Archive->Link.uniformSamplerOrImageUnitIndex.size()) {
return -1;
}
return record.Archive->Link.uniformSamplerOrImageUnitIndex[location];
}
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract - in particular why the
// composite band had to land in the same package as this function.
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) {
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
// THE RECORD FIRST, and through the BAND-AWARE reader: a composite pipeline's CSO
// lives in the applier's CompositeShaderCsos table, and asking the ordinary one for
// it would answer null for a program that is perfectly live.
if (PipeShaderCsoRecordForHandle(cso) == nullptr) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record on the handle arm, so "
"no driver program can be built for it and the draw keeps what is bound",
cso.Slot, cso.Gen);
return nullptr;
}
auto* slot = AdoptTwinByHandle(g_backendProgramObjects, cso, "shader CSO");
if (slot == nullptr) return nullptr;
if (!*slot) *slot = MakeShared<BackendProgramObjectImpl>();
return slot->get();
}
BackendProgramObjectImpl::BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
m_backendProgramId = g_GLESFuncs.glCreateProgram();
if (m_backendProgramId == 0) {
MGLOG_E_ONCE("Failed to create program object in backend.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Created backend program object with ID: %u", m_backendProgramId);
}
}
BackendProgramObjectImpl::~BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendProgramId != 0) {
MGLOG_D("Deleting backend program object with ID: %u", m_backendProgramId);
g_GLESFuncs.glDeleteProgram(m_backendProgramId);
// The driver may recycle this GL name for a future program; a stale
// guard entry would then wrongly skip the glUseProgram for it.
if (g_lastUsedBackendProgramId == m_backendProgramId) {
g_lastUsedBackendProgramId = 0;
}
}
}
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding) {
if (backendProgramId == 0 || blockName.empty()) return false;
if (!g_GLESFuncs.glGetProgramResourceIndex || !g_GLESFuncs.glShaderStorageBlockBinding) return false;
GLuint driverIndex =
g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK, blockName.c_str());
if (driverIndex == GL_INVALID_INDEX) {
// An arrayed block is enumerated per element by GL but declared once; the
// generated ESSL carries the bare block name.
const auto bracket = blockName.rfind('[');
if (bracket == String::npos || blockName.back() != ']') return false;
driverIndex = g_GLESFuncs.glGetProgramResourceIndex(backendProgramId, GL_SHADER_STORAGE_BLOCK,
blockName.substr(0, bracket).c_str());
if (driverIndex == GL_INVALID_INDEX) return false;
}
g_GLESFuncs.glShaderStorageBlockBinding(backendProgramId, driverIndex, binding);
return true;
}
void ReseedShaderStorageBlockBindings(Uint backendProgramId, const ProgramBuildSource& src) {
const auto& overrides = src.GetShaderStorageBlockBindingOverrides();
if (overrides.empty()) return; // the overwhelming majority of programs
for (const auto& [blockName, binding] : overrides) {
if (binding < 0) continue;
ApplyShaderStorageBlockBinding(backendProgramId, blockName, static_cast<Uint>(binding));
}
}
// P5e (pg). ON A PUSH BUILD THE SIGNATURE IS THE SOURCE'S, TAKEN VERBATIM, and it is not
// recomputed here for a reason worth stating: on the handle arm the number the DRAW PATH
// compares against is MGPipeShaderCsoRecord::Signature - the CLIENT's commutative hash,
// computed in MG_Impl/Pipe/ProgramEmit.h. If the server recomputed its own from the
// record's override tail, the two formulas would have to stay bit-identical for ever or
// every draw of every program with an override would rebuild. Taking the client's number
// verbatim removes that coupling: there is one authority, and the twin's stamp and the
// clean clause read the same field. The monolith arm's source carries the answer of the
// function this replaces, computed from the frontend map, so its behaviour is unchanged.
Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) {
return src.StorageOverrideSignature;
}
namespace {
// The GL internal format bound to an image unit right now. GL_NONE for a unit
// outside the frontend's array, which cannot be addressed at all.
Uint BoundImageUnitFormat(Int unit) {
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
// P5e (fb, §5.4): the format-less image bake asks what format the APPLICATION
// named at this unit, and under a transport the record is the only statement of
// it - MGPImageView::InternalFormat is that same GLenum, copied by the client
// out of the unit's own binding. A unit the applier has never been told about
// answers 0, which is what an unbound unit answers on the frontend arm too.
const auto& st = MG_Pipe::MGPipeApplier();
if (static_cast<SizeT>(unit) >= st.BoundShaderImages.size()) return 0;
return static_cast<Uint>(st.BoundShaderImages[static_cast<SizeT>(unit)].InternalFormat);
}
// Combines one (unit, format) pair into a running digest. Commutative, so the order
// the uniforms are walked in cannot change the answer, and mixed rather than summed
// so a unit and a format cannot trade places between two pairs and cancel out.
Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) {
Uint64 entry = static_cast<Uint64>(static_cast<Uint32>(unit)) + 0x9e3779b97f4a7c15ull;
entry ^= static_cast<Uint64>(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2);
return signature + entry;
}
// Pipeline position of a shader stage. Names the PRODUCER of an inter-stage
// interface block: a block one stage consumes was written by the stage before it.
// ShaderStage is declared in pipeline order, so the enum value IS the position;
// compute has no inter-stage interface at all and is reported as -1.
Int InterStagePipelineIndex(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex:
case ShaderStage::TessControl:
case ShaderStage::TessEval:
case ShaderStage::Geometry:
case ShaderStage::Fragment:
return static_cast<Int>(stage);
default:
return -1;
}
}
// Whether a stage can declare interface blocks in BOTH directions at once, i.e.
// whether one block name can name two different blocks inside it. A vertex INPUT
// and a fragment OUTPUT cannot be blocks and compute has neither, so only these
// three can. This is what keeps the module probe off every program without
// tessellation or geometry - which is every program Minecraft and its shader packs
// build.
Bool CanDeclareBlocksInBothDirections(ShaderStage stage) {
return stage == ShaderStage::TessControl || stage == ShaderStage::TessEval ||
stage == ShaderStage::Geometry;
}
// Reflection names an array uniform after its first element ("g_image[0]") at every
// location it spans; SPIR-V names the variable once, without the subscript. This is
// the name both sides agree on.
String ImageUniformBaseName(const String& reflectionName) {
if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) {
return reflectionName.substr(0, reflectionName.size() - 3);
}
return reflectionName;
}
// GL_MAX_<stage>_IMAGE_UNIFORMS as the ES driver reports it, which is also exactly what
// MobileGL advertises for it (GL_Getter answers from the same DynamicBackendParameters).
// -1 for a stage the ES side has no such limit for, which is the "cannot say" answer
// the diagnostic that reads it prints rather than a made-up number. [[maybe_unused]]
// because its only caller is an MGLOG_E argument, and MGLOG_E compiles to nothing in a
// build whose MOBILEGL_LOG_ACTIVE_LEVEL is above ERROR.
[[maybe_unused]] Int AdvertisedStageImageUniformLimit(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex:
return g_GLESCapabilities.MaxVertexImageUniforms;
case ShaderStage::TessControl:
return g_GLESCapabilities.MaxTessControlImageUniforms;
case ShaderStage::TessEval:
return g_GLESCapabilities.MaxTessEvaluationImageUniforms;
case ShaderStage::Geometry:
return g_GLESCapabilities.MaxGeometryImageUniforms;
case ShaderStage::Fragment:
return g_GLESCapabilities.MaxFragmentImageUniforms;
case ShaderStage::Compute:
return g_GLESCapabilities.MaxComputeImageUniforms;
default:
return -1;
}
}
// Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only
// through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes
// through untouched - the emitted ESSL still has to be legal for the driver.
Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRgba32f:
case glslang::ElfRgba16f:
case glslang::ElfR32f:
case glslang::ElfRgba8:
case glslang::ElfRgba8Snorm:
case glslang::ElfRgba32i:
case glslang::ElfRgba16i:
case glslang::ElfRgba8i:
case glslang::ElfR32i:
case glslang::ElfRgba32ui:
case glslang::ElfRgba16ui:
case glslang::ElfRgba8ui:
case glslang::ElfR32ui:
return true;
default:
return false;
}
}
// The GL internal format a glslang layout format names, for the eighteen non-core
// formats WidenImageFormatsForEssl carries losslessly plus nothing else: the only
// question asked of it is "does this DECLARED format widen", and answering 0 for
// everything else is the same "no" a non-widenable format gets. Kept as its own
// switch rather than routed through the frontend's enum converters because a
// TLayoutFormat is a glslang value and the reflection snapshot stores it raw.
//
// IT MUST LIST EXACTLY WHAT WideningOfSpirvImageFormat DOES. This table is what arms
// the pass (ImageFormatWillBeWidened -> declaresWidenableImageFormat), so a format the
// pass would carry but this switch answers 0 for never gets the chance: the module
// reaches SPIRV-Cross with its original qualifier, the throw takes the stage, and the
// only visible symptom is the "no GLSL ES spelling" diagnostic for a format that has
// one. That is exactly what r11f_g11f_b10f did until it was added here.
Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) {
switch (format) {
case glslang::ElfRg32f:
return 0x8230; // GL_RG32F
case glslang::ElfRg16f:
return 0x822F; // GL_RG16F
case glslang::ElfR16f:
return 0x822D; // GL_R16F
case glslang::ElfRg8:
return 0x822B; // GL_RG8
case glslang::ElfR8:
return 0x8229; // GL_R8
case glslang::ElfRg8Snorm:
return 0x8F95; // GL_RG8_SNORM
case glslang::ElfR8Snorm:
return 0x8F94; // GL_R8_SNORM
case glslang::ElfRg32i:
return 0x823B; // GL_RG32I
case glslang::ElfRg16i:
return 0x8239; // GL_RG16I
case glslang::ElfR16i:
return 0x8233; // GL_R16I
case glslang::ElfRg8i:
return 0x8237; // GL_RG8I
case glslang::ElfR8i:
return 0x8231; // GL_R8I
case glslang::ElfRg32ui:
return 0x823C; // GL_RG32UI
case glslang::ElfRg16ui:
return 0x823A; // GL_RG16UI
case glslang::ElfR16ui:
return 0x8234; // GL_R16UI
case glslang::ElfRg8ui:
return 0x8238; // GL_RG8UI
case glslang::ElfR8ui:
return 0x8232; // GL_R8UI
// Not a channel widening but a lossless re-encoding into rgba16f - the one entry
// here whose carrier has a different per-channel layout. See
// WidenImageFormatsPass.h.
case glslang::ElfR11fG11fB10f:
return 0x8C3A; // GL_R11F_G11F_B10F
// 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same
// channel count, every value representable. Only the transfer is re-encoded.
case glslang::ElfRgb10a2ui:
return 0x906F; // GL_RGB10_A2UI
// The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES.
// These are the entries whose carrier changes the shader-visible type as well as
// the qualifier (image2D becomes uimage2D), so every access through them is
// wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h.
case glslang::ElfRgba16:
return 0x805B; // GL_RGBA16
case glslang::ElfRg16:
return 0x822C; // GL_RG16
case glslang::ElfR16:
return 0x822A; // GL_R16
case glslang::ElfRgb10A2:
return 0x8059; // GL_RGB10_A2
case glslang::ElfRgba16Snorm:
return 0x8F9B; // GL_RGBA16_SNORM
case glslang::ElfRg16Snorm:
return 0x8F99; // GL_RG16_SNORM
case glslang::ElfR16Snorm:
return 0x8F98; // GL_R16_SNORM
default:
return 0;
}
}
// Whether the ESSL chain will re-declare an image of this format in a core carrier
// and mask its accesses (WidenImageFormatsForEssl). The same rule
// TextureImpl::GetImageBindableStorageWidening applies to the storage and the bind -
// the three layers move together or the shader addresses a texel size the storage
// does not have.
//
// Without GL_NV_image_formats there is no legal spelling for any non-core format, so
// everything carriable widens. WITH the extension only the formats SPIRV-Cross
// refuses to print do: it throws for its is_desktop_only_format set instead of
// emitting a token, and the throw loses the stage however willing the driver was.
Bool ImageFormatWillBeWidened(Uint glInternalFormat) {
if (glInternalFormat == 0) return false;
if (MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) == 0) {
return false;
}
return !g_GLESCapabilities.SupportsExtendedImageFormats ||
!MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(glInternalFormat);
}
} // namespace
// What the format bake needs from the frontend, collected in one walk of the uniform
// reflection: which image uniforms declared NO format (the only ones a bake may touch -
// a declared format is authoritative and stays), what the units they address currently
// hold, and whether any format in play - declared or baked - is outside the ES core set.
ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src) {
ImageFormatBakeInputs inputs;
// A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it
// with. There is no legal ESSL for such a shader at all, so the stage will not
// compile and the program is lost - a failure that used to leave nothing behind but
// a draw that rendered nothing. Recorded and reported ONCE per program build rather
// than per uniform: an image array reaches this decision once per element.
String unspellableUniform;
String unspellableFormat;
Uint unspellableCount = 0;
const auto recordUnspellableFormat = [&](const String& uniformName, String formatSpelling) {
if (unspellableCount == 0) {
unspellableUniform = uniformName;
unspellableFormat = Move(formatSpelling);
}
++unspellableCount;
};
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
const auto& type = src.GetUniformTypeFacts(loc);
if (type.hasFormat) {
// Declared, and therefore never overridden by the BAKE - but a non-core
// spelling still has to become legal ESSL somehow.
const auto declaredFormat = static_cast<glslang::TLayoutFormat>(type.layoutFormat);
if (!IsCoreEsslLayoutFormat(declaredFormat)) {
// Eighteen of the twenty-six non-core formats are re-declared in a core
// format that carries them losslessly, with every access masked back to
// the channels GL says they have (WidenImageFormatsForEssl, and the
// matching storage/bind widening in TextureImpl). Those need neither the
// extension nor the diagnostic: there IS a legal spelling for them now.
if (ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) {
inputs.declaresWidenableImageFormat = true;
} else {
inputs.needsExtendedImageFormats = true;
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
// From the OWNED TypeFacts, not from a live TType: the reflection
// snapshot already carries the declared layout format, and there
// is no glslang object to ask on a translation-cache L1 hit.
recordUnspellableFormat(name,
glslang::TQualifier::getLayoutFormatString(declaredFormat));
}
}
}
continue;
}
const Int unit = src.GetUniformSamplerOrImageUnitIndex(loc);
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue;
const Uint boundFormat = BoundImageUnitFormat(unit);
// Every format-less uniform contributes to the rebuild key, including one whose
// unit holds nothing yet: an image bound for the first time AFTER the link has
// to move the key, or the program built against "nothing bound" would never be
// rebuilt against the real format.
inputs.units.push_back(unit);
inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat);
if (boundFormat == 0) continue;
if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) {
// The same three-way split the DECLARED branch above makes, and it has to be
// the same one: a format-less image is baked with the bound format, so from
// WidenImageFormatsForEssl's point of view the two routes hand it identical
// modules and must arm it identically.
if (ImageFormatWillBeWidened(boundFormat)) {
// The bake writes this format INTO the module, so the widening that runs
// straight after has to be armed for it even though nothing DECLARED it -
// and armed WHETHER OR NOT the driver has GL_NV_image_formats. SPIRV-Cross
// throws for its is_desktop_only_format set the moment it targets ESSL,
// however willing the driver was, so the extension decides HOW MUCH gets
// widened (widenOnlyUnprintableImageFormats) and never WHETHER. Arming
// this only on the no-extension path left the shader half of the widening
// switched off while TextureImpl's storage/bind half - which keys on
// SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran:
// the stage threw, the program linked without it, and every dispatch
// silently did nothing. That is the whole of
// KHR-GL43.stencil_texturing.functional's compute half, whose uni_image is
// a format-less uimage2D bound to an R8UI texture.
inputs.declaresWidenableImageFormat = true;
} else if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
// Outside the GLSL ES core set, with no GL_NV_image_formats to spell it
// and no core format that carries it exactly: there is no legal ESSL for
// this stage at all. Leaving the image format-LESS is NOT a softer
// failure: all three test devices reject a format-less image declaration
// outright ("all images have to define layout format"), readonly and
// writeonly alike, so it trades one hard compile error for another. The
// unit stays in the rebuild key either way, so a rebind to a spellable
// format still rebuilds and works.
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which "
"GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and "
"no core format carries exactly.",
name.c_str(), unit, boundFormat);
recordUnspellableFormat(
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
continue;
} else {
inputs.needsExtendedImageFormats = true;
}
}
const String baseName = ImageUniformBaseName(name);
const auto existing = inputs.glFormatByUniformName.find(baseName);
if (existing == inputs.glFormatByUniformName.end()) {
inputs.glFormatByUniformName.emplace(baseName, boundFormat);
} else if (existing->second != boundFormat) {
// An ARRAY whose elements were pointed at units holding different formats.
// One declaration carries one qualifier, so there is no spelling for it, and
// the uniform is left format-less rather than given a format that is wrong
// for all but one element. Marked in place with GL_NONE and swept below -
// never by erasing here, because the entry is reached again by the array's
// remaining elements and a flat hash map must not be mutated structurally
// while an iterator into it is live.
existing->second = 0;
}
}
for (const auto& entry : inputs.glFormatByUniformName) {
if (entry.second == 0) inputs.conflictedNames.push_back(entry.first);
}
for (const auto& conflicted : inputs.conflictedNames) {
inputs.glFormatByUniformName.erase(conflicted);
}
// Split off the ones SPIRV-Cross will not print. They cannot go through the module -
// it throws for them when targeting ESSL, and the stage is lost - so they are spelled
// into the emitted text instead. Collected first, erased after, because a flat hash
// map must not be restructured while it is being walked.
Vector<String> textCompleted;
for (const auto& entry : inputs.glFormatByUniformName) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) {
continue;
}
// A format the widening carries stays on the module route even though SPIRV-Cross
// would not print it: by the time SPIRV-Cross sees the declaration it names the
// core carrier, which it does print. Writing the narrow spelling into the text
// instead would put back exactly the token the driver rejects.
if (ImageFormatWillBeWidened(entry.second)) {
continue;
}
String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second);
if (spelling.empty()) continue; // no image-format spelling at all; nothing to write
inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling));
textCompleted.push_back(entry.first);
}
for (const auto& name : textCompleted) {
inputs.glFormatByUniformName.erase(name);
}
// Unlatched MGLOG_E, like the transpile- and link-failure diagnostics in SyncToBackend:
// one line per failing program build, and naming the uniform and the format is the
// whole diagnostic value. Left as a log rather than a link failure on purpose - the
// frontend has already reported LINK_STATUS = true and GL cannot retract it, and the
// program stays queryable exactly as the "linked but not drawable" exit leaves it.
if (unspellableCount != 0) {
MGLOG_E("Image format '%s' on uniform '%s' has no GLSL ES spelling and this driver does not expose "
"GL_NV_image_formats%s; the stage using it cannot compile and the program will draw "
"nothing.",
unspellableFormat.empty() ? "(none)" : unspellableFormat.c_str(), unspellableUniform.c_str(),
unspellableCount > 1 ? " (and it is not the only image uniform affected)" : "");
}
return inputs;
}
// Every image ARRAY whose elements the application did NOT leave on units consecutive from
// element zero - the only shape ESSL can spell, since an image unit there comes solely
// from the one layout(binding=N) an array declaration carries. Desktop GL assigns them
// per element with glUniform1i, which ES makes an INVALID_OPERATION on an image uniform,
// so there is nothing to fix at the API end and the emitted text has to carry it
// (RemapImageArrayElementUnits). Empty for every program that does not do this, which is
// very nearly all of them - one walk of the reflection and no allocation in that case.
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) {
Vector<ImageArrayUnitPlan> plans;
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
// Reflection repeats the array's "g_image[0]" spelling at EVERY location the array
// spans, so only the location that name resolves back to is the array itself.
if (src.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
const String baseName = ImageUniformBaseName(name);
if (baseName == name) continue; // a scalar image: one binding says it all
ImageArrayUnitPlan plan;
plan.name = baseName;
for (Uint element = loc;
element <= maxUniformLoc &&
src.UniformLocationsAliasSameUniform(static_cast<Int>(loc), static_cast<Int>(element));
++element) {
plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element));
}
if (plan.units.size() < 2) continue;
Bool consecutive = true;
for (SizeT element = 0; element < plan.units.size(); ++element) {
if (plan.units[element] != plan.units[0] + static_cast<Int>(element)) {
consecutive = false;
break;
}
}
// What ESSL does unaided is already right; leaving these out is what keeps the
// emitted text of every ordinary image shader byte-identical to before.
if (consecutive) continue;
plans.push_back(Move(plan));
}
return plans;
}
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
Uint64 signature = 0;
for (const Int unit : m_formatlessImageUnits) {
signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit));
}
return signature;
}
Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const {
if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0;
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
}
// ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) =====
//
// One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the
// DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the
// boundary the L2 memo keys on is a function signature rather than a comment: every
// input this reads is either an argument below or a process-global capability bit,
// and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add
// it to BuildEsslTranslationKey too - an under-specified key here is a silently
// miscompiled shader.
//
// Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray,
// MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples,
// SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion}
// (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
//
// Deliberately NOT in here, and therefore NOT in the key: the text-level passes that
// follow in SyncToBackend. They are cheap string work and they read a long tail of
// live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject
// reflection, the norm-clamp masks and the fragColor broadcast count are live
// globals, the buffer-texture tier retargets an #extension line) whose inclusion
// would make the key both enormous and fragile for no measurable saving.
//
// Returns false when SPIRV-Cross refused the module; `outError` then holds its
// message and nothing is memoized.
Bool BackendProgramObjectImpl::TranspileSpirvToEssl(
const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const std::map<String, String>& inputBlockRenames, const std::map<String, String>& outputBlockRenames,
const Bool stripInputBlockLocations, const Bool stripOutputBlockLocations,
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
String& outError) const {
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross.
Vector<unsigned int> loweredSpirv;
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv,
enableSpirvValidation) &&
!loweredSpirv.empty()) {
effectiveSpirv = &loweredSpirv;
}
// A geometry stage that writes memory, on a driver that drops the writes it makes
// after EmitVertex(): every emit is deferred behind them (DeferGeometryEmitsPass). FIRST
// in the chain, while every output is still an Output variable - the viewport-index
// lowering below demotes one, and must see the replay's stores to it as well.
// Self-gated on the module (an emit and a memory write), so a geometry stage that
// writes nothing comes back untouched. Keyed by deferGeometryEmitsArmed.
Vector<unsigned int> deferredEmitSpirv;
if (glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit) {
bool deferred = false;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeferGeometryEmitsForEssl(
*effectiveSpirv, deferredEmitSpirv, deferred, enableSpirvValidation) &&
deferred && !deferredEmitSpirv.empty()) {
effectiveSpirv = &deferredEmitSpirv;
MGLOG_I_ONCE("DirectGLES is deferring a geometry stage's EmitVertex() calls behind its "
"memory writes, because this driver drops storage writes made after an emit.");
}
}
// GL_KHR_shader_subgroup where the ES driver has none (SubgroupPolicy.h): the compute
// stage's subgroup operations are lowered onto a 32-lane virtual subgroup in
// workgroup-shared memory, so the ESSL needs nothing from the driver but barrier()
// and shared memory. Where the driver has subgroups, the operations stay and
// SPIRV-Cross spells them as GL_KHR_shader_subgroup_* built-ins. iterationRP's
// scratch race is patched first unless MOBILEGL_ITRP_FIX is ForceOff, as
// on DirectVulkan. On a driver whose shared-memory loads go stale across barrier()
// the scratch is read through atomic loads. Keyed by subgroupEmulationArmed /
// iterationRPBarrierFixArmed / subgroupScratchAtomicReads.
Vector<unsigned int> barrierFixedSpirv;
Vector<unsigned int> emulatedSubgroupSpirv;
if (glShaderType == GL_COMPUTE_SHADER &&
ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated) {
if (ShouldApplyIterationRPFixes() &&
MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
*effectiveSpirv, barrierFixedSpirv, enableSpirvValidation) &&
!barrierFixedSpirv.empty()) {
effectiveSpirv = &barrierFixedSpirv;
}
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
*effectiveSpirv, emulatedSubgroupSpirv,
static_cast<Uint32>(g_GLESCapabilities.MaxComputeSharedMemorySize), enableSpirvValidation,
g_GLESCapabilities.SharedMemoryLoadsGoStale) &&
!emulatedSubgroupSpirv.empty()) {
effectiveSpirv = &emulatedSubgroupSpirv;
} else {
MGLOG_E("DirectGLES: subgroup emulation declined a compute stage; it keeps subgroup "
"operations this ES driver cannot compile");
}
}
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
// there IS an extension that provides it - so this runs only when the driver does
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
// gets the `#extension` request added to the decompiled source below instead.
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
// viewport 0), which is the degradation ViewportArrayScenario already documents
// for this backend; NOT demoting it costs the whole program, because the stage
// fails to compile and every draw made with it silently renders nothing.
// Gated on the module actually declaring the output, so no other stage pays an
// optimizer round trip for it.
// One parse of the module answers every armed pass gate below. The per-gate
// Declares* probes each cost a BuildModule per stage, and on a driver where both
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
// Probing the pre-lowering module is sound for both gates: demoting
// gl_ViewportIndex neither adds nor removes multisampled image types.
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
// this is backend code and must not reach into the GL frontend. 4 is that
// translation unit's kFrontendMaxSamples, which is the source of truth -
// keep the two in step.
const Int advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
// by default (MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
// reason to keep the builtin: it only ever gave the SHADER a compilable name, while
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
// rasterized every index as index 0 exactly like a driver without it. Lowering here
// is what lets the ESSL passes downstream turn the builtin into the flat varying the
// replay gates on.
//
// Restricted to the three stages GL lets WRITE the builtin (4.1 core gives it to the
// geometry stage, ARB_shader_viewport_layer_array adds vertex and tessellation
// evaluation). A fragment stage's gl_ViewportIndex is an INPUT, which the pass
// declines anyway, and a compute stage has none - so arming those two only ever
// bought them the shared probe's BuildModule for nothing.
const Bool stageCanWriteViewportIndex = glShaderType == GL_VERTEX_SHADER ||
glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER;
const Bool viewportLoweringArmed =
stageCanWriteViewportIndex &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
const Bool sampleClampArmed = g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
// The image-format widening is armed on EVERY driver, so its probe has to ride the
// shared parse rather than add one: it is asked of every stage of every program, and
// a BuildModule per stage per gate is exactly what cost compile-heavy CTS cases ~10%
// before this struct existed. What differs per driver is only HOW MUCH it widens -
// everything carriable where there is no GL_NV_image_formats to spell the narrow
// format, and only the formats SPIRV-Cross refuses to print where there is.
const Bool widenOnlyUnprintableImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats;
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
if (viewportLoweringArmed || sampleClampArmed) {
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(*effectiveSpirv);
}
Vector<unsigned int> loweredViewportSpirv;
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
!loweredViewportSpirv.empty()) {
effectiveSpirv = &loweredViewportSpirv;
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which ESSL has no core "
"spelling for. The builtin was demoted to a plain global; %s.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
ViewportArrayEmulationEnabled() ? "the ESSL passes below promote it to a routing varying"
: "every invocation renders into viewport 0");
}
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
// reports - but the realised allocation cannot be, and
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
// texture the ONE sample Adreno and Mali actually support for it. A shader
// written against the advertised ceiling then fetches a sample that storage does
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
// the Sample operand to the backend-real per-category maximum so the fetch lands
// inside the allocation. Gated on some category actually being squeezed AND the
// module actually declaring a multisampled image, so no other stage pays an
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
// allocates the sample count it was asked for, so its modules are already right.
Vector<unsigned int> clampedSampleSpirv;
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
*effectiveSpirv, clampedSampleSpirv, g_GLESCapabilities.MaxColorTextureSamples,
g_GLESCapabilities.MaxIntegerSamples, g_GLESCapabilities.MaxDepthTextureSamples,
advertisedMaxSamples, enableSpirvValidation, ClampsUnsignedMultisampleFetchesAtRuntime()) &&
!clampedSampleSpirv.empty()) {
effectiveSpirv = &clampedSampleSpirv;
}
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
// rather than emulating them, so this has to happen before it sees the binary.
Vector<unsigned int> splitArrayInputSpirv;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
// Only when the pass ACTUALLY split something. The optimizer hands back a
// re-serialised copy either way, and adopting that copy for every vertex
// shader would put every one of them through a round trip they do not need
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
// first time this gate was missing (the original capture; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024).
effectiveSpirv = &splitArrayInputSpirv;
}
// Adopt the rewritten module only when THIS stage actually had one of the
// blocks - the optimizer hands back a re-serialised copy either way, and taking
// that copy for a module it did not rewrite is not free (it cost the
// create-indirect retrace 0.15 SSIM when the array-input split first missed
// this gate; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). The report has to be
// per stage, not cumulative: a fragment shader consuming the same block reports a
// name the vertex stage already reported,
// and its own rewrite must still be taken or the two stages stop matching.
Vector<unsigned int> flattenedXfbSpirv;
if (!xfbCaptureBlockNames.empty()) {
// Reported into a local first, and published only if the module is really
// adopted. The caller unions the published set unconditionally (so that a
// cache HIT contributes its names too), so publishing a name for a rewrite
// that was declined would rename a capture the emitted ESSL never renamed.
std::set<String> flattenedNames;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
*effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv,
enableSpirvValidation) &&
!flattenedXfbSpirv.empty() && !flattenedNames.empty()) {
effectiveSpirv = &flattenedXfbSpirv;
outFlattenedXfbBlockNames = Move(flattenedNames);
}
}
// The producer-keyed interface-block rename, planned program-wide by the caller and
// applied to this stage: the blocks it CONSUMES are spelled after the previous stage
// present in the program and the ones it PRODUCES after itself, so a tessellation
// evaluation stage's two TCSOutputBlocks stop being one name and every other stage
// still agrees with it. See UniquifyIoBlockNamesPass for why Mali needs it.
//
// INSIDE THE MEMOIZED SEGMENT, and at exactly the position it was written in - after
// the XFB flatten, before the UBO precision strip. Both halves of that matter:
// * INSIDE, because it rewrites the MODULE and the emitted ESSL carries the result.
// Left outside, a second program sharing this stage's key would be served ESSL
// with the blocks un-renamed and the repair would silently stop working - the
// same trap SetAtomicCounterBlockBindings sets one screen down.
// * AT THIS POSITION, because moving a SPIR-V pass in a chain is a behavioural
// change, and this one arrived device-verified on Mali. Hoisting it above the
// cache probe instead would have needed no key material at all (the rename would
// already be in the module bytes the key hashes) and was rejected for that
// reason: it reorders the chain, and it would re-serialise the module on every
// build including the ones the memo is there to make free.
// Its two rename maps are therefore KEY MATERIAL - see the caller.
Vector<unsigned int> uniquifiedIoBlockSpirv;
if (!inputBlockRenames.empty() || !outputBlockRenames.empty()) {
std::set<String> stageRenamedIoBlockNames;
if (MG_Util::ShaderTranspiler::ShaderCompiler::UniquifyIoBlockNamesForEssl(
*effectiveSpirv, inputBlockRenames, outputBlockRenames, stageRenamedIoBlockNames,
uniquifiedIoBlockSpirv, enableSpirvValidation) &&
!uniquifiedIoBlockSpirv.empty() && !stageRenamedIoBlockNames.empty()) {
effectiveSpirv = &uniquifiedIoBlockSpirv;
MGLOG_D("Program %u stage %s: %zu inter-stage interface block(s) renamed per "
"producing stage, because some stage of this program declares the same "
"block name in both directions and the ES driver may alias the two.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
stageRenamedIoBlockNames.size());
}
}
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
// ForceSupporterOutput swaps the fragment header to highp, that member reads
// back as highp and the ES driver refuses to link ("definitions of uniform
// block ... do not match"). Strip the hint from block structs so both stages
// declare the member highp; nothing else about emission changes.
Vector<unsigned int> uboPrecisionSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
!uboPrecisionSpirv.empty()) {
effectiveSpirv = &uboPrecisionSpirv;
}
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
// rejects the require. So on such devices emulate screen-linear interpolation instead
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
// the decoration - exact, extension-free. Devices that have the extension keep the
// decoration and let the hardware do it natively.
Vector<unsigned int> noperspectiveSpirv;
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
!noperspectiveSpirv.empty()) {
effectiveSpirv = &noperspectiveSpirv;
}
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
// than approximating one. The shared pass turns the type into the 2D one and
// divides the coordinate of every normalized-coordinate lookup by the texture
// size, which is the whole of the difference between the two.
Vector<unsigned int> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv,
enableSpirvValidation) &&
!rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv;
}
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
// as 2D for images without ever asking whether the type is arrayed, so a
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
// two-component constructor, which every driver rejects, taking the whole
// program with it. The pass does the conversion properly - type to 2D array,
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
Vector<unsigned int> arrayImageSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv, arrayImageSpirv,
enableSpirvValidation) &&
!arrayImageSpirv.empty()) {
effectiveSpirv = &arrayImageSpirv;
}
// The SAMPLER half of the same 1D story, and a defect one layer deeper than the one
// above. SPIRV-Cross DOES widen a 1D sampler's coordinate for ES - it just prints the
// OFFSET and the two GRADIENT operands with the arity the desktop shader spelled, so
// textureLodOffset(sampler1DArray, vec2, float, int) is emitted against a
// sampler2DArray and the driver answers "no matching overloaded function found",
// losing the stage and silently no-oping every dispatch that used it. Widening the
// operands alone would be an INVALID module (the validator derives the required arity
// from the image's own Dim), so the pass moves the type to 2D and widens coordinate,
// offset and gradients together.
//
// NO KEY MATERIAL, by the same test LegalizeResourceArrayIndexingForEssl passes:
// it takes the module and nothing else, no capability bit arms it, and it self-gates
// on the module's own content (BinaryHasOffsetOrGrad1DSampledImage). The module is
// already the largest thing in the L2 key, so it is covered completely.
Vector<unsigned int> sampled1DSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DSampledImagesForEssl(*effectiveSpirv, sampled1DSpirv,
enableSpirvValidation) &&
!sampled1DSpirv.empty()) {
effectiveSpirv = &sampled1DSpirv;
}
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
// format"), which loses the whole program. Give each such image the format the
// application bound to its unit - the one GL's format-class rules make correct -
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
// also rewrites image types, so this one is looking at the final shapes.
//
// Gated on the module actually declaring one: the map is empty for every program
// whose images all declare formats, and the cheap probe keeps a program that has
// an unbound format-less image from paying an optimizer round trip per stage.
Vector<unsigned int> imageFormatSpirv;
if (!imageFormatBake.glFormatByUniformName.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv, enableSpirvValidation) &&
!imageFormatSpirv.empty()) {
effectiveSpirv = &imageFormatSpirv;
}
// GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven
// reach ES only through GL_NV_image_formats, which no tested driver advertises. A
// shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws
// for some of them and the driver rejects the token for the rest ("'rg32f' : not a
// legal layout qualifier id"), and dropping the qualifier is refused too ("all images
// have to define layout format") - so the stage is lost and every draw with the
// program silently renders nothing. Seventeen of them widen EXACTLY into a core format
// of the same per-channel width, and this rewrites those declarations to the carrier
// and masks every access back to the channels GL says the format has. The other nine
// have no exact carrier and keep the honest diagnostic
// CollectImageFormatBakeInputs emits.
//
// A driver that HAS GL_NV_image_formats still needs part of this. SPIRV-Cross throws
// for its is_desktop_only_format set when it targets ESSL rather than printing a
// token, and the throw loses the stage however willing the driver was - llvmpipe
// advertises the extension and `layout(r8ui) uimage2D` lost its whole program there
// until the widening ran for it too. So the driver bit decides HOW MUCH is widened,
// never WHETHER.
//
// AFTER the bake above, deliberately: a format-less image whose unit holds a non-core
// format is baked with that format and widened here, so both routes end in the same
// place and there is no second widening rule for baked declarations.
//
// KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which selects the
// mode - see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes
// no other input: what it rewrites is a pure function of the module's own declared
// formats and that mode, and the module is already the largest thing in the L2 key.
// The ARMING flag is deliberately NOT key material: it only decides whether the pass
// runs, and the module below is adopted only when the pass actually changed the bytes
// - so a program-wide flag that over-arms a stage costs an optimizer round trip and
// changes no output.
//
// DirectVulkan is deliberately not given this: it takes the declared format natively
// and resolves the descriptor's view format from the same bind state.
Vector<unsigned int> widenedImageFormatSpirv;
if (imageFormatBake.declaresWidenableImageFormat &&
MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl(
*effectiveSpirv, widenedImageFormatSpirv, widenOnlyUnprintableImageFormats,
enableSpirvValidation) &&
!widenedImageFormatSpirv.empty() && widenedImageFormatSpirv != *effectiveSpirv) {
effectiveSpirv = &widenedImageFormatSpirv;
}
// GLSL ES demands a constant integral expression to index a fragment output
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
// outright ("array indexes for fragment outputs must be constant integral
// expressions"), linking no program and silently no-oping every draw that
// uses it. llvmpipe accepts it, ANGLE does not - which is the whole of the
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
// on the ESSL path only: the same module is legal for DirectVulkan.
Vector<unsigned int> outputIndexSpirv;
if (glShaderType == GL_FRAGMENT_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
!outputIndexSpirv.empty()) {
effectiveSpirv = &outputIndexSpirv;
}
// Same rule, two more resources, every stage: desktop GL lets an array of
// storage blocks and an array of image uniforms be indexed with any
// dynamically-uniform expression, GLSL ES keeps the ES 3.1
// constant-expression rule for both, and the drivers enforce it - Qualcomm
// ("indexing into an SSBO array using a non-constant expression is not
// permitted"), llvmpipe ("image arrays indexed with non-constant expressions are
// forbidden in GLSL ES") - losing the stage, the program, and every draw or
// dispatch that used it, while the frontend keeps reporting the link glslang
// performed. Fold or lower the index here, on the ESSL path only: the same
// module is legal for DirectVulkan, which binds the array as one descriptor
// array.
//
// The image half is also what makes RemapImageArrayElementUnits below possible
// at all: that pass rewrites `g_image[k]` into a per-element declaration, and it
// can only do that once every k the emitted ESSL spells is a literal.
//
// NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the
// module and nothing else - no capability bit arms it, no per-program plan steers
// it - and it self-gates on the module's own content
// (BinaryHasDynamicResourceArrayIndexing). The module is already the largest
// thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl,
// whose signature is equally module-only but which SupportsViewportArray ARMS -
// that bit is in the key precisely because of it.
Vector<unsigned int> blockArrayIndexSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeResourceArrayIndexingForEssl(
*effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) &&
!blockArrayIndexSpirv.empty()) {
effectiveSpirv = &blockArrayIndexSpirv;
}
// glslang kept the application's layout(offset = N) on the atomic counters it
// lowered onto gl_AtomicCounterBlock_<N>, and no std140/std430 layout can put
// member 0 anywhere but offset 0 - so SPIRV-Cross throws ("cannot be expressed as
// neither std430 nor std140") and the stage never reaches the driver. Collapse the
// block into one uint array at offset 0 and re-index each counter to the element
// that used to be at its byte offset; the buffer then stays bound whole, which it
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 64 on Adreno 830 and 32 or
// more everywhere else, so an 8-byte bind offset is not expressible on any of them).
// BEFORE SetAtomicCounterBlockBindings
// below, which only moves the block's BINDING and needs the block intact.
//
// NO KEY MATERIAL either, for the same reason - and note where the application's
// layout(offset = N) values live: glslang already baked them into the module as
// member Offset decorations, so they are in the key as MODULE BYTES. There is no
// separate offset input to carry.
Vector<unsigned int> atomicCounterSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(
*effectiveSpirv, atomicCounterSpirv, enableSpirvValidation) &&
!atomicCounterSpirv.empty()) {
effectiveSpirv = &atomicCounterSpirv;
}
// The second half of the inter-stage interface-block repair, and the one that
// actually closes the 420pack group: this driver drops the payload of a block that
// carries an explicit layout(location=) whenever a tessellation or geometry stage
// is in the pipeline, so the qualifier comes off and ES matches the block by name
// and member sequence instead. The names those two sides agree on are the ones the
// rename above just fixed, which is why this runs AFTER it and not before.
//
// The caller arms the two directions; both are false unless the driver POST
// measured the defect AND this program has a stage that can hit it. Adopted only
// when this stage really had a located block, for the reason the array-input split
// documents: the optimizer hands back a re-serialised copy either way.
//
// LAST IN THE CHAIN, and that position is load-bearing. Vulkan SPIR-V REQUIRES a
// Location on every user-defined Input/Output variable
// ([VUID-StandaloneSpirv-Location-04915]), so the module this produces is
// deliberately no longer valid Vulkan SPIR-V - it is an ESSL-emission intermediate
// that goes straight into SPIRV-Cross and reaches no driver as SPIR-V. Running it
// here means no later pass validates what it produced; the pass itself skips
// validation for the same reason (see StripIoBlockLocationsForEssl). Anywhere
// earlier and every remaining pass would latch a validation failure on a module
// that is doing exactly what it was asked to.
Vector<unsigned int> strippedIoBlockLocationSpirv;
if (stripInputBlockLocations || stripOutputBlockLocations) {
Bool strippedAny = false;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripIoBlockLocationsForEssl(
*effectiveSpirv, stripInputBlockLocations, stripOutputBlockLocations, strippedAny,
strippedIoBlockLocationSpirv, enableSpirvValidation) &&
!strippedIoBlockLocationSpirv.empty() && strippedAny) {
effectiveSpirv = &strippedIoBlockLocationSpirv;
// THE ARMING SIGNAL, and it is INFO on purpose: the per-stage line below is
// MGLOG_D, which is compiled out of every build CI and the device runs, so
// nothing outside a debug build could tell an armed repair from a silently
// un-armed one. Latched, so it costs one line per process rather than one
// per stage of every program. The integration lane that pins the emulation
// on asserts on exactly this line - see UnlocatedIoBlockScenario.
MGLOG_I_ONCE("DirectGLES is emitting inter-stage interface blocks WITHOUT their "
"layout(location) qualifier, because this driver loses a located "
"block's payload across a tessellation or geometry boundary.");
MGLOG_D("Program %u stage %s: interface-block location qualifiers dropped "
"(%s), because this driver loses a located block's payload across a "
"tessellation or geometry boundary.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
stripInputBlockLocations
? (stripOutputBlockLocations ? "consumed and produced" : "consumed")
: "produced");
}
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, ResolveBackendEsslVersion());
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
// then deliberately preserves the qualifier for `buffer` declarations.
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
}
// Atomic counters, same mechanism for the same reason. glslang already turned
// every atomic_uint into a member of gl_AtomicCounterBlock_<N> and let the IO
// mapper pick that block's binding, which has no relation to the GL binding point
// N the application bound its counter buffer to - and can alias an SSBO the
// application binds itself. Move each block to its reserved slot and record N, so
// the draw path knows which GL_ATOMIC_COUNTER_BUFFER points to re-issue as
// storage-buffer bindings.
//
// BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding
// this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings`
// is an OUTPUT this stage produces and the draw path consumes, so it is in the L2
// payload. A hit that replayed only the text would leave the bindings empty and
// every counter buffer unbound - the same class of silent loss the flattened XFB
// block names would have been.
spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop, outAtomicCounterGlBindings);
// `layout(index = 0)` is the GL default spelled out loud, and GLSL ES has no such
// qualifier in core - a stage that prints it is refused with "index layout
// qualifier requires EXT_blend_func_extended" and the whole program then draws
// nothing. Drop the decoration when it carries the default; a REAL dual-source
// index (1) is left alone, because that one genuinely needs the extension and the
// driver has to see it. Fragment stage only: no other stage can carry it.
if (glShaderType == GL_FRAGMENT_SHADER) {
spvcSession.DropDefaultFragmentOutputColorIndex();
}
// `readonly writeonly` together says the buffer variable can only be asked its
// .length(), which the frontend has already enforced - so the pair is inert, and
// printing it is not. llvmpipe's ES compiler refuses a block spelled that way and the
// stage never reaches the program.
spvcSession.RelaxReadWriteExclusiveStorageBuffers();
// gl_ShadingRateEXT / gl_PrimitiveShadingRateEXT: SPIRV-Cross spells them only for
// Vulkan GLSL, so they go through under a sentinel name and are restored below. Only
// a program whose stages use them gets here with anything to do, and only a driver
// with GL_EXT_fragment_shading_rate gets such a program past the frontend.
const Bool restoreShadingRateBuiltins = spvcSession.PrepareShadingRateBuiltinsForEssl();
const char* result = nullptr;
spvcSession.Compile(&result);
if (!result) {
// The caller owns the diagnostic: it is the one that knows the
// frontend program id, and a failed transpile must NOT be memoized -
// the message names the stage and is worth re-emitting every time.
const char* lastError = spvcSession.GetLastErrorString();
outError = lastError ? lastError : "";
return false;
}
outSource = result;
if (restoreShadingRateBuiltins) {
MG_Util::ShaderTranspiler::SpvcSession::FinishShadingRateBuiltinsForEssl(outSource);
}
return true;
}
// GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL
// shader: the input patch is passed through unmodified and the levels come from the
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such
// state and no such allowance - it rejects the program at link, and with an EMPTY info
// log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only:
// link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and
// atomic counter the failing conformance case also declares). The frontend's own glslang
// link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every
// draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and
// an untouched SSBO, with no error anywhere.
//
// So the missing stage is synthesized and attached here, alongside the program's own.
// DirectVulkan already does exactly this for the same structural reason
// (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather
// than inventing an approach.
//
// Nothing that works today can be harmed by it: it fires ONLY for a program that has an
// evaluation stage and no control stage, and every such program fails to link on ES right
// now. The worst case is that the synthesized stage fails to compile or link, which leaves
// the program exactly as dead as it already was - but with a driver log that says why,
// where today there is an empty one.
void BackendProgramObjectImpl::AttachPassthroughTessControlStage(
const ProgramBuildSource& src, const Int tessEvalShaderIndex,
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
const String& tessEvalStageEssl) {
// PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output
// patch size - so a program built for one value is stale for another. Recorded here
// and compared on the draw path (SyncCurrentProgram), the same shape as the
// storage-block and image-format signatures next to it.
const Uint patchVertices = MG_Pipe::gPipeInputs->IsLive() ? MG_Pipe::gPipeInputs->GetPatchVertices() : 3u;
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
// into the same stage (ES has no such state and no entry point to forward them to), so
// they are recorded and compared alongside the patch size - the two move together, as
// BuildPassthroughTessControlEssl's contract says.
m_passthroughTessControlOuterLevel = MG_Pipe::gPipeInputs->IsLive()
? MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel()
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = MG_Pipe::gPipeInputs->IsLive()
? MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel()
: FloatVec2(1.0f, 1.0f);
if (tessEvalShaderIndex < 0 || static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no "
"SPIR-V for it; the pass-through control stage GL describes cannot be checked, so "
"the program is left to fail its ES link.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
// The one shape the pass-through cannot stand in for. It forwards gl_Position and
// nothing else, so an evaluation stage that reads a user-defined varying or a
// per-patch input - both of which carry a Location, where every built-in it needs
// does not - would start reading undefined values the moment a control stage sat
// between it and the vertex stage. Declining keeps that from being silent; it is the
// identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed.
if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput(
shaderSpirvs[static_cast<SizeT>(tessEvalShaderIndex)])) {
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a "
"user-defined input through it; a synthesized pass-through control stage cannot "
"forward that, so the program is declined rather than fed an undefined varying.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
// Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares
// gl_PerVertex, and with which members, depends on what the application's shaders
// touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage
// it feeds is an ES link error against a program with no other problem. gl_in copies
// the vertex stage's OUT block (that is what arrives) and gl_out the evaluation
// stage's IN block (that is what is expected). A neighbour that redeclared nothing
// yields an empty list, which leaves the driver's own built-in declaration in place -
// which is exactly what matching it requires.
const String inMembers = ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String());
const String outMembers =
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
String source =
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers,
m_passthroughTessControlOuterLevel, m_passthroughTessControlInnerLevel);
// The mirrored member lists can carry gl_PointSize - the neighbour stage declared it,
// so matching it is the whole point - and a redeclaration is exactly as illegal as a
// reference in ESSL without the extension. Same directive, same never-speculative
// rule as the per-stage loop; a driver with neither spelling gets nothing added and
// fails below with its own message, which is the honest outcome for a shape it
// cannot express.
const char* passthroughPointSizeExtension =
source.find("gl_PointSize") != String::npos
? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true)
: nullptr;
source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension);
// Same for gl_ClipDistance/gl_CullDistance mirrored out of a vertex stage that
// writes them: the neighbour could only compile with the extension, so it exists.
source = RequestClipCullDistanceExtension(Move(source));
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
if (backendShaderId == 0) {
MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for "
"program %u.",
src.GetExternalIndex());
m_backendProgramUsable = false;
return;
}
const char* sourceCStr = source.c_str();
MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices "
"%u):\n%s",
src.GetExternalIndex(), patchVertices, sourceCStr);
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
// GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten
// out-param must read as "compile failed" and never as a silent success.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
GLint logLength = 0;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for "
"program %u. Driver log: %s\nSource:\n%s",
src.GetExternalIndex(), log.data(), sourceCStr);
m_backendProgramUsable = false;
g_GLESFuncs.glDeleteShader(backendShaderId);
return;
}
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
// Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is
// what makes the program own it and what keeps a relink from leaking it.
g_GLESFuncs.glDeleteShader(backendShaderId);
}
// ---- THE PROGRAM BUILD, AND THE ONE SOURCE IT READS (P5e pg) ----------------------
//
// ONE BODY, TWO HEADS, AND THE PULL BUILD'S HEAD IS THE ORIGINAL ONE. In the pull build
// `ProgramBuildSource` IS `MG_State::GLState::ProgramObject`, this function keeps its
// name, its signature and its null guard, and `src` is a reference to the object it was
// already dereferencing on every line - so the pull build compiles the same reads
// through the same accessors and G1's byte identity survives a rename and nothing else.
//
// In a PUSH build the body is a private worker that the two public overloads feed:
// SyncToBackend(program) - the monolith-glue half, ruling 1 - wraps the frontend's own
// archive, and SyncToBackendByHandle(cso) wraps the RECORD's. That is the whole of this
// family's rule F: after this, nothing in a program build dereferences a frontend
// object, and the forty accessor calls below read memory the server owns.
void BackendProgramObjectImpl::SyncToBackendFromSource(const ProgramBuildSource& src) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Recorded before either early return below, so Use() can always name the GL
// program a no-op draw belongs to - including the "linked but not drawable" exit.
m_frontendProgramId = src.GetExternalIndex();
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
// queryable, but has no SPIR-V to build a driver program out of. GL cannot retract
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
// answer, and this is where the ES backend expresses it.
if (!src.GetLinkStatus() || !src.GetSpirvStatus()) {
MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
"program ID: %u",
src.GetExternalIndex());
return;
}
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u", src.GetExternalIndex(),
m_backendProgramId);
// Every link-derived cache below (incl. m_samplerUniformBindings and its
// lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt;
// the sampler-pass memo keyed on them must not survive.
m_samplerPassMemo.valid = false;
m_backendProgramUsable = true;
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
// The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the
// transpile loop below), so the set they were generated against is part of what makes
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src);
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
// The top is snapshotted here so every stage of this program - and the draw path
// reading it afterwards - resolves the same slot for the same GL binding.
m_atomicCounterGlBindings.clear();
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// Re-established by AttachPassthroughTessControlStage below when this program needs
// one; cleared first so a program that stops needing one (a relink that now attaches
// a real control stage) does not keep comparing against a stale patch size. The
// default levels are re-established from the same call and gated on the same -1.
m_passthroughTessControlPatchVertices = -1;
m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
// The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
// Taken BEFORE the transpile loop so both the bake and the key see one snapshot.
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(src);
m_formatlessImageUnits = imageFormatBake.units;
m_imageUnitFormatSignature = imageFormatBake.signature;
for (const auto& conflicted : imageFormatBake.conflictedNames) {
MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with "
"different bound formats; left format-less.",
conflicted.c_str(), src.GetExternalIndex());
}
// ...and once more for image ARRAYS whose per-element units are not consecutive, which
// ESSL has no way to express in one declaration. Program-wide, like the bake, and read
// from the same snapshot of the reflection; the per-stage rewrite happens below.
const Vector<ImageArrayUnitPlan> nonConsecutiveImageArrays = CollectNonConsecutiveImageArrayPlans(src);
// Detach all existing shaders
GLint attachedCount = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_ATTACHED_SHADERS, &attachedCount);
MGLOG_D("Currently attached shaders count: %d", attachedCount);
if (attachedCount > 0) {
Vector<GLuint> attachedShaders(attachedCount);
// Every GL out-param in this function is pre-initialized and every count is
// re-clamped after the query. A driver that returns without writing the
// out-param (no current context, a lost context, a stubbed entry point) would
// otherwise leak an uninitialized stack value straight into a container size
// or a loop bound - which is exactly how this path used to throw
// length_error out of a Vector fill-ctor.
GLsizei actualCount = 0;
g_GLESFuncs.glGetAttachedShaders(m_backendProgramId, attachedCount, &actualCount,
attachedShaders.data());
actualCount = std::clamp<GLsizei>(actualCount, 0, static_cast<GLsizei>(attachedShaders.size()));
MGLOG_D("Detaching %d existing shaders from program %u", actualCount, m_backendProgramId);
for (GLsizei i = 0; i < actualCount; ++i) {
MGLOG_D("Detaching shader ID: %u from program %u", attachedShaders[i], m_backendProgramId);
g_GLESFuncs.glDetachShader(m_backendProgramId, attachedShaders[i]);
}
}
// Attach current shaders.
//
// The EXECUTABLE's stage list, not GetAttachedShaders(): this loop indexes
// shaderSpirvs by the same running index, and the generated SPIR-V is a link
// artifact while the attach list is live. GL 4.6 core 7.3 makes glAttachShader take
// effect only at the next link, so a program that is attached to after it linked has
// MORE entries in the attach list than there are modules - and pairing the two read
// straight off the end of shaderSpirvs (a std::vector copy from garbage, which is
// how this crashed). GetLinkedShaderStages() is the list the modules were generated
// from, one entry per module, in module order.
const Vector<ShaderStage> linkedStages = src.GetLinkedShaderStages();
auto& shaderSpirvs = src.GetGeneratedSpirv();
// Both come from the same Link(), so they agree by construction. If they ever did
// not there would be no index this function could safely use for EITHER array, so
// this refuses the build instead of picking one and hoping.
if (linkedStages.size() != shaderSpirvs.size()) {
MGLOG_E_ONCE("Program %u: %zu linked stage(s) but %zu generated SPIR-V module(s); refusing to "
"build a backend program from mismatched link artifacts.",
src.GetExternalIndex(), linkedStages.size(), shaderSpirvs.size());
m_backendProgramUsable = false;
return;
}
if (src.PointSizeDemoted()) {
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that
// pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because
// every rendering assertion stays green on a healthy driver whether the
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was "
"demoted to an ordinary varying, because this driver cannot host the built-in "
"in those stages.");
}
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
for (const auto& ref : src.GetLinkedShaderSnapshot()) {
if (!ref.shader) continue;
const auto& stage =
MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(ref.shader->GetShaderStage()));
// The source THIS link consumed, which a later glShaderSource does not disturb.
const String& src = ref.source ? *ref.source : ref.shader->GetShaderSource();
MGLOG_D("Original src @ %s: \n", stage.c_str());
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
}
const Bool enableSpirvValidation = src.GetSpirvValidationEnabled();
// Blocks a transform-feedback capture request names a member of ("StageData" of
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
// then captures nothing at all for it, so those blocks - and ONLY those - get
// flattened into per-member variables below, in EVERY stage, so a producer and its
// consumer keep matching. gl_PerVertex members ("gl_Position") carry no block
// prefix and so never enter this set.
std::set<String> xfbCaptureBlockNames;
for (const auto& xfbVarying : src.GetTransformFeedbackVaryings()) {
const SizeT dot = xfbVarying.name.find('.');
if (dot != String::npos && dot > 0) {
xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot));
}
}
std::set<String> flattenedXfbBlockNames;
// Stages whose ESSL had a read+write image declaration doubled into a coherent
// read/write pair, and by how many. Empty for every program but a handful; consulted
// ONLY when the link then fails, because the doubling spends the driver's per-stage
// GL_MAX_*_IMAGE_UNIFORMS budget that MobileGL keeps advertising unadjusted (halving
// the advertised value would fail basic-api and NotSupported-out cases that never pay
// the doubling, so the limit must stay honest and the connection has to be made here
// instead). See the budget note on SplitReadWriteImageUniforms.
struct SplitImageUniformStage {
ShaderStage stage;
Uint splitCount;
};
Vector<SplitImageUniformStage> splitImageUniformStages;
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface
// blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at
// the same time - which the tessellation evaluation stage of both interface-block
// tests in KHR-GL42/43.shading_language_420pack does ("in TCSOutputBlock ... out
// TCSOutputBlock"). SPIRV-Cross keeps the same split (block_input_names vs
// block_output_names) and re-emits BOTH under the name FOO, so the generated ESSL
// declares two different blocks called FOO in one shader. Adreno's ES compiler
// keeps them apart; Mali's does not - the stage compiles, the program links, and
// the output block's payload never reaches the next stage. All 22 of that group's
// Mali failures are exactly the two tests that write this shape, and every one of
// them passes on Adreno and on DirectVulkan.
//
// The repair is a rename keyed on the PRODUCING stage, planned here and applied
// per stage below so a producer and its consumer keep naming the same block.
// Gated twice over, because a re-serialised module is not free (it cost the
// create-indirect retrace 0.15 SSIM the first time the array-input split missed
// its gate; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024): only a tessellation
// or geometry stage can declare blocks in both directions at all, and even then the
// probe has to FIND a collision before any
// stage is rewritten.
std::set<String> collidingIoBlockNames;
std::set<String> declaredIoBlockNames;
Vector<Int> stagePipelineIndices(linkedStages.size(), -1);
Bool anyStageCanDeclareBlocksInBothDirections = false;
for (SizeT index = 0; index < linkedStages.size(); ++index) {
const ShaderStage stage = linkedStages[index];
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true;
}
// A SECOND, INDEPENDENT interface-block repair riding the same gate, because it
// needs the same question answered: "does this program have a stage where an
// inter-stage block can go wrong?". CanDeclareBlocksInBothDirections is true for
// exactly the tessellation and geometry stages, which is also exactly the set of
// stages whose presence makes this driver drop a LOCATED block's payload (a
// vertex-to-fragment located block is fine on the same driver, measured). The two
// repairs are otherwise unrelated: the rename fixes a name collision inside ONE
// stage, this drops a qualifier from EVERY block of the program - so it does not
// wait for the collision probe to find anything.
const Bool ioBlockLocationStripArmed =
!g_GLESCapabilities.SupportsLocatedInterStageIoBlocks && anyStageCanDeclareBlocksInBothDirections;
if (anyStageCanDeclareBlocksInBothDirections) {
for (SizeT index = 0; index < shaderSpirvs.size(); ++index) {
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
shaderSpirvs[index], collidingIoBlockNames, declaredIoBlockNames);
}
// A block a capture request names is resolved BY NAME at
// glTransformFeedbackVaryings time - and flattened away entirely by the pass
// below - so renaming one would ask the driver for a block the request does
// not spell.
for (const auto& xfbCaptureBlockName : xfbCaptureBlockNames) {
collidingIoBlockNames.erase(xfbCaptureBlockName);
}
}
// The one spelling every stage of THIS program agrees on for `blockName` as written
// by pipeline stage `producerPipelineIndex`. "__" is reserved in GLSL, so a name
// already ending in '_' does not get another one, and the digit-suffix loop steps
// off any name the program already spells.
const auto uniqueIoBlockName = [&declaredIoBlockNames](const String& blockName, Int producerPipelineIndex) {
const char* separator = (!blockName.empty() && blockName.back() == '_') ? "" : "_";
String candidate = blockName + separator + "mgio" + std::to_string(producerPipelineIndex);
while (declaredIoBlockNames.find(candidate) != declaredIoBlockNames.end()) {
candidate += "0";
}
return candidate;
};
// Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a
// program that has an evaluation stage without one, with an empty info log. When that
// is this program's shape, one is synthesized below - and it has to be spelled to
// MATCH the two stages it sits between, so their emitted ESSL is kept here as it is
// produced. Empty for every program that has a control stage of its own, which is
// all but a handful.
Bool hasTessEvalStage = false;
Bool hasTessControlStage = false;
Int tessEvalShaderIndex = -1;
String vertexStageEssl;
String tessEvalStageEssl;
//
// Asked of the executable for the same reason the loop below indexes it: a
// tessellation evaluation shader merely ATTACHED to a linked vertex+fragment program
// is not part of what this program runs, and synthesizing a control stage for it
// would build a tessellating driver program for an executable that does not
// tessellate (and would take tessEvalShaderIndex past the end of shaderSpirvs).
for (SizeT index = 0; index < linkedStages.size(); ++index) {
const ShaderStage stage = linkedStages[index];
if (stage == ShaderStage::TessControl) hasTessControlStage = true;
if (stage == ShaderStage::TessEval) {
hasTessEvalStage = true;
tessEvalShaderIndex = static_cast<Int>(index);
}
}
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
// The stage order the loop below walks, with every FRAGMENT stage moved to the end.
// The viewport-routing gate is the reason: whether a fragment stage needs one is a
// question about the OTHER stages ("does any of them still write gl_ViewportIndex?"),
// and the honest, free answer to it is the promotion the producing stage's own text
// pass just performed. Answering it any other way costs a BuildModule per
// pre-rasterization stage of every program - the parse the shared SpirvGateFeatures
// probe exists to avoid. Nothing else in the loop is order-sensitive: the two
// passthrough-tessellation sources it captures are a vertex and an evaluation stage,
// and the three sets it accumulates are unions.
//
// The pre-rasterization stages are walked in PIPELINE order (the stage enum's), so a
// consumer's input-block redeclaration below can mirror the output block its producer
// has already emitted.
Vector<SizeT> stageOrder;
stageOrder.reserve(linkedStages.size());
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] != ShaderStage::Fragment) stageOrder.push_back(index);
}
std::stable_sort(stageOrder.begin(), stageOrder.end(), [&linkedStages](SizeT a, SizeT b) {
return static_cast<Int>(linkedStages[a]) < static_cast<Int>(linkedStages[b]);
});
for (SizeT index = 0; index < linkedStages.size(); ++index) {
if (linkedStages[index] == ShaderStage::Fragment) stageOrder.push_back(index);
}
// The output gl_PerVertex members the last pre-rasterization stage emitted, for the
// next stage's input redeclaration (RedeclareClipCullDistanceInputsInPerVertexBlock).
// A synthesized pass-through control stage copies its input block through, so the
// vertex stage's members are what an evaluation stage behind one sees.
String producerOutPerVertexMembers;
// Set by whichever pre-rasterization stage's demoted mg_ViewportIndex global the text
// pass turned into a varying; read by the fragment stage to decide whether to inject
// the gate that consumes it.
Bool programRoutesViewportIndex = false;
// No fragment stage, no gate - and without a gate the promotion below would only add
// an output nothing can read. That is not merely useless: in a separable program
// pipeline the fragment stage lives in a DIFFERENT program, which never saw this
// build and cannot be given a gate, so promoting there would hang an unmatched
// varying off a program to buy nothing. Both cases keep the pre-emulation behaviour,
// which is what a program with no fragment stage had anyway.
const Bool programHasFragmentStage =
std::find(linkedStages.begin(), linkedStages.end(), ShaderStage::Fragment) != linkedStages.end();
const Bool viewportEmulationForThisProgram = ViewportArrayEmulationEnabled() && programHasFragmentStage;
for (const SizeT index : stageOrder) {
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(linkedStages[index]);
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
if (backendShaderId == 0) {
MGLOG_E_ONCE("Failed to create backend shader for attachment.");
continue;
}
String source;
auto& spirvCode = shaderSpirvs[index];
// A samplerBuffer is core in the OpenGL 3.1+ context MobileGL advertises but needs
// ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits
// `#extension GL_EXT_texture_buffer : require` and the driver rejects both that
// and the isamplerBuffer keyword - the program never links and every draw using it
// becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched:
// this is bounded by program count, and which stage failed is the whole point.
// Gated on the capability so the module walk never runs on a healthy driver.
if (!AreBufferTexturesSupported() &&
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) {
MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver "
"cannot provide (%s). The shader will not compile and the program will "
"not link; every draw using it is a no-op.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
GetBufferTextureTierName());
m_backendProgramUsable = false;
g_GLESFuncs.glDeleteShader(backendShaderId);
continue;
}
// ---- L2 of the shader translation memo -------------------------------
// The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage,
// memoized on the module bytes and on every capability bit and per-program
// input that steers them. See TranslationCache.h for the key inventory and
// for why the text-level passes below stay outside the boundary.
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
esslKeyInputs.spirv = &spirvCode;
esslKeyInputs.shaderType = glShaderType;
// The EFFECTIVE arming, computed the same way TranspileSpirvToEssl computes it.
// Duplicated rather than shared because the two live on opposite sides of the
// memo boundary - and a key that disagrees with the pass it is keying is the one
// failure mode of this cache that renders wrong pixels instead of being slow.
esslKeyInputs.viewportIndexLoweringArmed =
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER ||
glShaderType == GL_GEOMETRY_SHADER) &&
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
esslKeyInputs.deferGeometryEmitsArmed =
glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit;
esslKeyInputs.subgroupEmulationArmed =
glShaderType == GL_COMPUTE_SHADER &&
ResolveEsprytSubgroupImplementation(g_GLESCapabilities) == SubgroupImplementation::Emulated;
esslKeyInputs.subgroupEmulationScratchBytes =
esslKeyInputs.subgroupEmulationArmed ? g_GLESCapabilities.MaxComputeSharedMemorySize : 0;
esslKeyInputs.iterationRPBarrierFixArmed =
esslKeyInputs.subgroupEmulationArmed && ShouldApplyIterationRPFixes();
esslKeyInputs.subgroupScratchAtomicReads =
esslKeyInputs.subgroupEmulationArmed && g_GLESCapabilities.SharedMemoryLoadsGoStale;
esslKeyInputs.supportsNoperspectiveInterpolation =
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
esslKeyInputs.supportsExtendedImageFormats = g_GLESCapabilities.SupportsExtendedImageFormats;
esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples;
esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples;
esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples;
esslKeyInputs.advertisedMaxSamples = std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames;
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop;
// THIS STAGE's share of the program-wide interface-block rename plan built above
// the loop. Resolved here, outside the memoized segment, because it is planning
// and not translation - exactly like the image-format bake map - and because that
// makes the two maps a plain function argument the L2 key can carry.
//
// KEYING ON THE RESOLVED MAPS rather than on what they were derived from
// (collidingIoBlockNames, declaredIoBlockNames, stagePipelineIndices, this
// stage's index) is deliberate: the maps ARE the pass's arguments, so they are
// exactly as fine as the pass's behaviour and no finer. Two programs whose
// collision plans differ but whose maps for THIS stage come out identical really
// do produce the same ESSL and should share the entry.
//
// A block whose other end is NOT in this program is deliberately left out of the
// plan: in a separate-shader-objects pipeline the interface it matches across
// lives in another program that never saw this plan, and renaming one side of
// THAT would break a program pipeline to repair a driver quirk. That is what the
// producer/consumer presence tests below are for - in a monolithic program both
// are trivially satisfied for every interface the collision can touch.
std::map<String, String> inputBlockRenames;
std::map<String, String> outputBlockRenames;
if (!collidingIoBlockNames.empty() && stagePipelineIndices[index] >= 0) {
const Int myPipelineIndex = stagePipelineIndices[index];
Int producerPipelineIndex = -1;
Bool hasConsumerStage = false;
for (const Int otherPipelineIndex : stagePipelineIndices) {
if (otherPipelineIndex < 0) continue;
if (otherPipelineIndex < myPipelineIndex && otherPipelineIndex > producerPipelineIndex) {
producerPipelineIndex = otherPipelineIndex;
}
if (otherPipelineIndex > myPipelineIndex) hasConsumerStage = true;
}
for (const auto& collidingBlockName : collidingIoBlockNames) {
if (producerPipelineIndex >= 0) {
inputBlockRenames[collidingBlockName] =
uniqueIoBlockName(collidingBlockName, producerPipelineIndex);
}
if (hasConsumerStage) {
outputBlockRenames[collidingBlockName] =
uniqueIoBlockName(collidingBlockName, myPipelineIndex);
}
}
}
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
esslKeyInputs.outputBlockRenames = &outputBlockRenames;
// ...and THIS STAGE's share of the interface-block LOCATION strip, planned the
// same way and for the same reason. The gate has three parts, all of which have
// to hold before a single block loses its qualifier:
// * the driver POST measured the defect (never a renderer-string quirk list);
// * this program has a stage that can hit it - a located block between a
// vertex and a fragment stage works on the affected driver, so a program
// with neither tessellation nor geometry keeps its ESSL byte for byte;
// * for THIS stage and THIS direction, this program HAS a stage on that side
// of it. That is the same test the rename plan above makes, and the same
// approximation: it asks "is some stage of this program earlier/later than
// me", not "is the exact partner of every one of my blocks here". The two
// coincide for every program MobileGL builds, because a separable pipeline
// is flattened into one composite carrying every stage that has a shader
// (GLContext::GetProgramForDraw) and a program bound with glUseProgram has
// no partner program at all - so a stage set with a gap in it does not
// arise. Should one ever arise, this must become the nearest-stage
// resolution the rename plan computes, or the two ends of the gap would
// disagree about the qualifier.
// The direction tests deliberately mirror that plan rather than inventing a
// second rule for the same question.
Bool stripInputBlockLocations = false;
Bool stripOutputBlockLocations = false;
if (ioBlockLocationStripArmed && stagePipelineIndices[index] >= 0) {
const Int myPipelineIndex = stagePipelineIndices[index];
for (const Int otherPipelineIndex : stagePipelineIndices) {
if (otherPipelineIndex < 0) continue;
if (otherPipelineIndex < myPipelineIndex) stripInputBlockLocations = true;
if (otherPipelineIndex > myPipelineIndex) stripOutputBlockLocations = true;
}
}
esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations;
esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations;
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey;
if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) {
esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs);
}
std::set<String> stageFlattenedXfbBlockNames;
// Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the
// transpile appends to this, on a hit the payload supplies it, and only then
// is it folded into the program-wide vector. Pointing the transpile straight
// at the member would have made the miss path and the hit path disagree about
// who owns the append.
Vector<Int> stageAtomicCounterGlBindings;
const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
if (esslHit) {
source = esslHit->essl;
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings;
} else {
String transpileError;
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames, imageFormatBake,
storageBlockBindingOverrides, inputBlockRenames, outputBlockRenames,
stripInputBlockLocations, stripOutputBlockLocations,
m_atomicCounterEsslBindingTop, enableSpirvValidation, source,
stageFlattenedXfbBlockNames, stageAtomicCounterGlBindings,
transpileError)) {
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
// below: one line per failing stage is bounded by program count and
// naming the stage is the entire diagnostic value. A stage that never
// reaches the driver leaves the program short of that stage, so the link
// fails with an EMPTY driver info log - the least debuggable failure
// MobileGL can produce, and what hid the whole
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
"SPIRV-Cross error: %s",
src.GetExternalIndex(), MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
transpileError.c_str());
m_backendProgramUsable = false;
continue;
}
if (esslCacheKey.Valid()) {
auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
payload->essl = source;
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
payload->atomicCounterGlBindings = stageAtomicCounterGlBindings;
const SizeT payloadBytes = MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
esslCache.Insert(esslCacheKey,
MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)),
payloadBytes);
}
}
// Per stage, never cumulative: a fragment shader consuming the same block
// reports a name the vertex stage already reported, and its own rewrite must
// still be taken or the two stages stop matching. Done here rather than inside
// the transpile so a cache HIT contributes its names too.
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(), stageFlattenedXfbBlockNames.end());
// Same rule for the atomic-counter bindings this stage declared, and for the
// same reason: the loop below de-duplicates across stages, so a hit that
// contributed nothing would silently drop a counter buffer the draw path has
// to bind.
m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(), stageAtomicCounterGlBindings.begin(),
stageAtomicCounterGlBindings.end());
// SPIRV-Cross sizes written clip/cull distances with a standalone
// `out float gl_ClipDistance[N];`, which an ES compiler may refuse as a reserved
// name and so lose the whole program. Redeclared as gl_PerVertex members instead.
// BEFORE the gl_PointSize extension probe below (the block can name it) and
// before vertexStageEssl is captured, so a synthesized pass-through control
// stage mirrors the block this produces.
source = RedeclareClipCullDistancesInPerVertexBlock(std::move(source), glShaderType);
// ...and the consumer half: a stage reading gl_in[i].gl_ClipDistance/CullDistance
// redeclares its input block with the sizes the producer wrote.
source = RedeclareClipCullDistanceInputsInPerVertexBlock(std::move(source), glShaderType,
producerOutPerVertexMembers);
if (glShaderType != GL_FRAGMENT_SHADER) {
producerOutPerVertexMembers =
ExtractPerVertexBlockMembers(source, /*input=*/false).value_or(String());
}
// Position in the chain is arbitrary: this is the only header-level rewrite, it
// edits #extension directives and never the body, and the replacement is the
// same length and stays an #extension line - so it commutes with every pass
// below, including ForceSupporterOutput's scan for the last directive. First,
// because a header concern reads better before the body ones.
source = RetargetTextureBufferExtension(std::move(source), g_GLESCapabilities.TextureBufferSupport);
// The other header-level rewrite, and next to that one for the same reason. The
// formats it covers are both the ones the bake above put into the module and the
// ones the application declared itself - either can be outside the thirteen GLSL
// ES has in core, and neither reaches the driver without this directive.
source =
RequestExtendedImageFormats(std::move(source), imageFormatBake.needsExtendedImageFormats &&
g_GLESCapabilities.SupportsExtendedImageFormats);
// The third header-level rewrite, for the builtin SPIRV-Cross prints bare:
// gl_ViewportIndex is in no version of ESSL core, so without this directive the
// stage does not compile and the whole program - not just its viewport routing -
// is lost. The token probe keeps the line off every other program and the
// capability gate keeps it off drivers that would hard-error on an unadvertised
// name; a driver without the extension took the LowerViewportIndexPass fallback
// above and its source no longer names the builtin at all, so the two are mutually
// exclusive by construction. Read `source` BEFORE it is moved from.
// The routing emulation is the third way this can be reached and the only one
// that needs no directive: it renames the fragment stage's read onto the varying
// the producing stage now writes, a few passes below.
const Bool needsViewportArrayExtension =
g_GLESCapabilities.SupportsViewportArray &&
!(ViewportArrayEmulationEnabled() && programRoutesViewportIndex) &&
source.find("gl_ViewportIndex") != String::npos;
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
// The fourth header-level rewrite, and the same shape as the third: ESSL has no
// gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the
// stages core and still leaves the built-in behind EXT/OES_..._point_size - while
// SPIRV-Cross prints it bare. Without the directive the stage fails to compile
// with "`gl_PointSize' undeclared", which takes the whole program to program 0:
// the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture
// of anything at all off that program silently comes back empty. The token probe
// keeps the line off every other program and PointSizeExtensionName returns
// nullptr - i.e. nothing is emitted - on a driver advertising neither spelling.
if (source.find("gl_PointSize") != String::npos) {
const Bool tessellationStage =
glShaderType == GL_TESS_CONTROL_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER;
if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) {
const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport
: g_GLESCapabilities.GeometryPointSizeSupport;
const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage);
if (pointSizeExtension == nullptr) {
// Latched, and an ERROR rather than a warning: what follows is a
// driver compile failure whose text names a built-in the application
// never mis-spelled, and the reason is a missing driver capability
// rather than anything in the shader. Saying so here is the whole
// difference between a legible skip and an unexplained black draw.
MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size "
"extension, so its ESSL has no gl_PointSize in a %s stage; program %u "
"will fail to compile. Point size from a non-vertex stage is not "
"available on this device.",
tessellationStage ? "tessellation" : "geometry",
tessellationStage ? "tessellation" : "geometry", src.GetExternalIndex());
}
source = RequestPointSizeExtension(std::move(source), pointSizeExtension);
}
}
source = RebindImageUniformsToFrontendUnits(std::move(source), src);
// The completion half of the format bake, for the formats SPIRV-Cross throws on
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
// program whose format-less images bound a format the module could carry, which
// is the normal case - those were baked into the SPIR-V above and this pass finds
// their declarations already qualified. AFTER the rebind, so the layout qualifier
// it edits is the one that already exists; BEFORE the split and the binding
// strip, so both halves of a split image inherit the format.
source = BakeImageFormatQualifiers(std::move(source), imageFormatBake.esslFormatQualifierByUniformName);
// An image ARRAY whose elements do not sit on consecutive units cannot be spelled
// by the single layout(binding=N) the rebind above stamped: ESSL gives element k
// the unit N+k and there is no glUniform1i to correct it with. Split the array
// into one scalar declaration per element, each with its own binding. AFTER the
// rebind and the format bake, both of which look the array up by its GL uniform
// name and need the binding already there; BEFORE the read+write split, so an
// element that is both read and written is split with its own binding on it.
if (!nonConsecutiveImageArrays.empty()) {
Vector<String> declinedImageArrays;
source = RemapImageArrayElementUnits(source, nonConsecutiveImageArrays, &declinedImageArrays);
for (const auto& declined : declinedImageArrays) {
// MGLOG_E, unlatched, like the transpile- and compile-failure diagnostics
// around it: this is the "linked, drew, produced wrong numbers, said
// nothing" shape that cost earlier waves whole days, and one line per
// declined array is bounded by program count. There is no honest GL answer
// to give instead - the frontend has already reported LINK_STATUS = true.
MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach "
"from one declaration, so this stage will read and write the WRONG units. State "
"program ID: %u, stage: %s.",
declined.c_str(), src.GetExternalIndex(),
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
}
}
// Wedged between those two on purpose:
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
// both halves of a split image is already the frontend texture unit (and so
// that pass never has to reason about the alias it introduces);
// * BEFORE RemoveLayoutBinding, whose keepBindingRegex recognises an image
// declaration and preserves its binding - an image unit cannot be set from
// the API in ES, so the qualifier is the only binding mechanism there is,
// and both halves of the pair have to still be carrying theirs when it runs.
// Takes no stage: the qualifier it adds is a decision about THIS text's accesses
// and the rename that keeps two stages from declaring one image uniform
// differently is keyed on that same decision, so two stages that agree still
// share one uniform (see the location-budget note on the pass).
Uint splitImageUniformCount = 0;
source = SplitReadWriteImageUniforms(source, &splitImageUniformCount);
if (splitImageUniformCount != 0) {
splitImageUniformStages.push_back({linkedStages[index], splitImageUniformCount});
}
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = LowerZeroGradCubeArrayShadowLookups(source, glShaderType);
source = EmulateTextureLodBias(source, ShouldAvoidExplicitLodBiasOnAngleLlvmpipe());
if (ClampsUnsignedMultisampleFetchesAtRuntime()) {
source = ClampUnsignedMultisampleFetches(source);
}
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
// The two halves of the gl_ViewportIndex routing emulation, next to the draw-
// parameter promotion because they are the same shape: a builtin ESSL cannot
// spell, demoted to a plain global by a SPIR-V pass, given a real interface here.
// BEFORE ForceSupporterOutput, so the `precision highp` statements it hoists to
// the top land above the declarations these inject; AFTER
// ForceFlatIntegerVaryings, which matches only declarations carrying a
// layout(...) qualifier and so cannot touch either of them.
if (viewportEmulationForThisProgram) {
if (glShaderType == GL_FRAGMENT_SHADER) {
if (programRoutesViewportIndex && !InjectViewportIndexPassGate(source)) {
// MGLOG_E, unlatched, like the transpile- and compile-failure
// diagnostics around it: the program still links and still draws, so
// nothing else in the process will ever say that its viewport routing
// silently collapsed back to one rectangle.
MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no "
"entry point to gate, so the routing cannot be emulated: every "
"index will rasterize against viewport 0. State program ID: %u.",
m_backendProgramId, src.GetExternalIndex());
}
} else if (PromoteViewportIndexGlobalToVarying(source)) {
programRoutesViewportIndex = true;
}
}
source = ForceSupporterOutput(source);
source = ClampNormFallbackOutputs(std::move(source), glShaderType, m_snormFallbackClampOutputMask,
m_unormFallbackClampOutputMask);
// Patch for Photon compiler precision issue
String findStr = "1000000.0";
String replaceStr = "65500.0";
auto pos = source.find(findStr);
while (pos != String::npos) {
MGLOG_D("Applying patch #2 to Photon...");
source.replace(pos, findStr.length(), replaceStr);
pos = source.find(findStr, pos);
}
const char* sourceCStr = source.c_str();
MGLOG_D("Setting shader source for backend shader ID: %u\nsrc:\n%s", backendShaderId, sourceCStr);
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed"
// and take the diagnostic path, never as a silent success that attaches an
// uncompiled shader.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
GLint logLength = 0;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
// +1 and zero-filled: GL_INFO_LOG_LENGTH already counts the terminator,
// but a driver that reports 0 (or fails the query) must still leave
// log.data() a readable empty C string for the %s below.
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
log.back() = '\0';
// MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering
// compiled E and W out of every INFO build: the Android retrace artifact
// carried 294 INFO lines and zero ERROR lines while two generated shaders
// were being rejected outright, and the lane could not say why it was
// rendering an empty translucent layer. A shader the driver refuses is
// never noise.
//
// A BOUNDED EXCERPT of the source goes with it. The driver log names a line
// and a column in text that exists nowhere but here, so without any source at
// all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole
// library at DEBUG - but the full dump cannot go at E either. This is not
// "one line per refused shader": SyncToBackend's rebuild gate keys on
// per-draw state (the enabled-draw-buffer count among it), so a program used
// across passes with different draw-buffer counts re-transpiles, re-compiles
// and re-fails on every alternation, i.e. per frame. At E - live at the
// production INFO level - each of those records would push the whole
// post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto
// /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps
// the record O(1); the full text is still there at D, printed against this
// same backend shader id by the "Setting shader source" line above, so
// nothing needs to be dumped twice.
constexpr SizeT kMaxLoggedSourceBytes = 2048;
String truncatedSource;
const char* sourceForLog = source.c_str();
if (source.size() > kMaxLoggedSourceBytes) {
// Back up to a line boundary when there is one inside the window, so the
// excerpt ends on a whole statement rather than mid-token. Built only on
// this branch: a stage that fits keeps its own buffer and is not copied.
SizeT cut = kMaxLoggedSourceBytes;
if (const SizeT lastNewline = source.rfind('\n', cut);
lastNewline != String::npos && lastNewline > 0) {
cut = lastNewline + 1;
}
truncatedSource = source.substr(0, cut);
truncatedSource += "... [" + std::to_string(source.size() - cut) +
" more bytes; the whole stage is printed at the DEBUG level]\n";
sourceForLog = truncatedSource.c_str();
}
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
"%u, driver log: %s\nSource:\n%s",
src.GetExternalIndex(), MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
backendShaderId, log.data(), sourceForLog);
m_backendProgramUsable = false;
// Nothing will ever attach this one, so nothing else can free it.
g_GLESFuncs.glDeleteShader(backendShaderId);
continue;
}
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
// Hand the shader's lifetime to the program, immediately and unconditionally.
//
// glDeleteShader only FLAGS a shader; the driver frees it when it is attached to
// nothing. Flagging it here is what makes the program own it, so deleting the
// program (or the detach loop above, on a relink) is what actually frees it.
// Without this call every program build leaked its shader objects for the process
// lifetime, and a relink leaked them twice - the detach loop above dropped the
// program's reference to shaders nothing had flagged, so they became unreachable
// AND undeletable. The GL swizzle conformance test builds 1,296 programs per case,
// so a handful of cases left tens of thousands of live driver shaders behind and
// the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*).
// Same class of defect as the missing framebuffer/renderbuffer/sampler destructors
// fixed in Wave 1, and the last of that family: this is the one backend GL object
// MobileGL creates without an owning wrapper to destroy it.
g_GLESFuncs.glDeleteShader(backendShaderId);
// Kept AFTER every text-level pass, so what the synthesized control stage mirrors
// is the text the driver actually sees, not an intermediate form.
if (needsPassthroughTessControl) {
if (glShaderType == GL_VERTEX_SHADER) {
vertexStageEssl = source;
} else if (glShaderType == GL_TESS_EVALUATION_SHADER) {
tessEvalStageEssl = source;
}
}
MGLOG_D("Processed shader source length: %zu", source.length());
}
if (needsPassthroughTessControl) {
AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs, vertexStageEssl,
tessEvalStageEssl);
}
// A counter buffer declared by several stages was recorded once per stage; the draw
// path binds per GL binding point, so collapse the duplicates here rather than
// re-issuing the same glBindBufferBase two or three times every draw.
if (!m_atomicCounterGlBindings.empty()) {
std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end());
m_atomicCounterGlBindings.erase(
std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()),
m_atomicCounterGlBindings.end());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
// frontend's requested names carry over unchanged.
SizeT declaredXfbVaryingCount = 0;
if (src.GetTransformFeedbackVaryingCount() > 0 && g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
const auto& xfbVaryings = src.GetTransformFeedbackVaryings();
Vector<const GLchar*> xfbNames;
xfbNames.reserve(xfbVaryings.size());
// A block this build flattened no longer HAS the member the application asked
// for; it has the variable that replaced it. Everything else - including a
// member of a block that was left alone - keeps the application's spelling.
// Storage first, pointers after: xfbNames holds pointers into these strings.
//
// Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer
// spells the built-in at all - the value lives in the carrier the demotion
// named - so the driver-side request has to follow it there. Only when the
// capture stage IS a demoted one (geometry, else evaluation): a program whose
// capture stage is the vertex shader keeps the built-in and its spelling,
// whatever happened to a control stage behind it.
Bool captureStageDemoted = false;
if (src.PointSizeDemoted()) {
for (const ShaderStage linkedStage : linkedStages) {
if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) {
captureStageDemoted = true;
break;
}
}
}
Vector<String> rewrittenXfbNames(xfbVaryings.size());
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
String flatName;
if (!flattenedXfbBlockNames.empty() &&
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
rewrittenXfbNames[nameIndex] = std::move(flatName);
} else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") {
rewrittenXfbNames[nameIndex] =
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME;
} else {
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
}
}
for (const auto& xfbName : rewrittenXfbNames) {
xfbNames.push_back(xfbName.c_str());
}
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), m_backendProgramId);
// Bounded: a lost context never answers GL_NO_ERROR, and this runs on the
// thread that would then spin forever.
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {}
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
xfbNames.data(), src.GetTransformFeedbackBufferMode());
// Unchecked before. A rejected capture set leaves the program linking happily
// with NO capture set at all, and then every draw of every span records
// nothing while the application reads its buffer's pre-draw bytes and
// GL_NO_ERROR - the signature four conformance families were stuck on.
if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) {
String declared;
for (const auto& xfbName : rewrittenXfbNames) {
if (!declared.empty()) declared += ", ";
declared += xfbName;
}
MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with "
"%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.",
m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(),
MG_Util::ConvertGLEnumToString(src.GetTransformFeedbackBufferMode()).c_str(),
declared.c_str(), src.GetExternalIndex());
}
declaredXfbVaryingCount = xfbNames.size();
}
// Link program
MGLOG_D("Linking program %u", m_backendProgramId);
g_GLESFuncs.glLinkProgram(m_backendProgramId);
GLint linkStatus = GL_FALSE;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
if (linkStatus != GL_TRUE) {
GLint logLength = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
if (logLength < 0) logLength = 0;
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
g_GLESFuncs.glGetProgramInfoLog(m_backendProgramId, logLength, nullptr, log.data());
log.back() = '\0';
// MGLOG_I for the same reason as the compile failure above: a program that
// links nothing no-ops every draw that uses it, and that has to be readable
// in an INFO-level artifact.
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
src.GetExternalIndex(), m_backendProgramId, log.data());
// The one link failure MobileGL can name a cause for that the driver's log never
// will: ESSL has no legal single declaration for a read+write image outside
// r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up
// declaring more image uniforms than the application did - against a
// GL_MAX_*_IMAGE_UNIFORMS that is still the driver's raw number, because lowering
// it would fail basic-api and NotSupported-out every case that only ever uses
// readonly/writeonly images. A shader declaring more than half a stage's budget in
// read+write images therefore links here and nowhere else, and without this line
// the next reader has only a generic driver message to go on.
for (const SplitImageUniformStage& split : splitImageUniformStages) {
MGLOG_E("...and %u read+write image uniform(s) in stage %s were split into coherent "
"read/write pairs, so that stage declares %u image uniform(s) more than the "
"program did; GL_MAX_*_IMAGE_UNIFORMS for it is %d. If the driver log names "
"image uniforms, that is the cause.",
split.splitCount,
MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(split.stage)).c_str(),
split.splitCount, AdvertisedStageImageUniformLimit(split.stage));
}
} else {
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
// A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a
// requested name the transpiled shader does not actually declare by simply not
// capturing it, and a program whose last vertex-processing stage was rewritten
// by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the
// synthesized pass-through tessellation control stage) can end up spelling its
// outputs differently from the frontend's request. Asking the driver what it
// ACTUALLY linked is the only way to tell that apart from a driver that just
// captures nothing - which is the whole ambiguity the empty-capture failures
// across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on.
if (declaredXfbVaryingCount > 0) {
GLint linkedXfbVaryings = 0;
GLint linkedXfbBufferMode = 0;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS, &linkedXfbVaryings);
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE,
&linkedXfbBufferMode);
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {}
const GLenum requestedMode = src.GetTransformFeedbackBufferMode();
if (static_cast<SizeT>(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount ||
static_cast<GLenum>(linkedXfbBufferMode) != requestedMode) {
MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not "
"agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) "
"in mode %s. Captures made with it will be empty or wrongly laid out.",
m_backendProgramId, src.GetExternalIndex(), declaredXfbVaryingCount,
MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(static_cast<GLenum>(linkedXfbBufferMode)).c_str());
} else {
MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s",
m_backendProgramId, linkedXfbVaryings,
MG_Util::ConvertGLEnumToString(static_cast<GLenum>(linkedXfbBufferMode)).c_str());
}
}
}
// The driver program was relinked IN PLACE, so its GL name no longer identifies
// the executable behind it - and that name is exactly what Use()'s
// g_lastUsedBackendProgramId early-out treats as identifying it. Without this,
// a rebuild of the program that is already bound issues no glUseProgram at all
// and the driver keeps running whatever the last one installed.
//
// GL 4.6 core 7.3 does promise that a successful re-link of a program in use
// installs the new executable - but only "for all shader stages where the program
// is active", and a stage the previous link did not produce is not active for
// anything. So a relink that ADDS a stage is precisely the case the promise does
// not cover. Verified with no MobileGL in the process (bare EGL + GLES 3.2,
// llvmpipe 26.1.4): vertex+fragment linked, used and drawn renders; a geometry
// shader attached and relinked reports LINK_STATUS true with an empty info log,
// and the next draw renders NOTHING and raises no error - while the same draw
// after a fresh glUseProgram of the same name renders again.
//
// A flag rather than zeroing the guard: 0 is also the id Use() binds for a build
// that did NOT come out usable, and a zeroed guard would make it skip that
// glUseProgram(0) and leave the failed program's previous executable running -
// the silent wrong-shader draw Use() exists to prevent.
m_rebindAfterRelink = true;
m_baseInstanceUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_UNIFORM_NAME);
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
m_baseVertexUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_VERTEX_UNIFORM_NAME);
m_baseInstanceWordIndexUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
// Asked of the DRIVER rather than remembered from the injection, deliberately: the
// gate is only real if the uniform survived compilation and linking, and this is the
// one question whose answer covers both. A gate the driver optimized away would
// otherwise leave the draw path replaying passes whose mask reaches nothing, which
// renders every index's primitives in every pass.
m_viewportPassMaskUniformLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, VIEWPORT_PASS_MASK_UNIFORM_NAME);
if (m_viewportPassMaskUniformLocation >= 0) {
// Sticky, and never cleared on a relink: it only ever short-circuits a per-draw
// check, so being late to go false costs a pointer compare and being late to go
// true would cost correctness.
g_anyProgramRoutesViewportIndex = true;
}
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
// SSBO blocks after compile); record it so draws bind the indirect buffer there.
m_indirectParamsBinding = -1;
if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) {
const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex(
m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME);
if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) {
m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1;
}
}
// Create global UBO
if (src.GetUBOSize() > 0) {
g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId);
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId);
g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, src.GetUBOSize(), nullptr, GL_STREAM_DRAW);
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
} else {
m_backendGlobalUBOId = 0;
}
CacheResourceLocations(src);
// NOT the mechanism that makes a rebinding work - the transpiled qualifier above is.
// glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes,
// so this replay is a no-op almost everywhere; it stays because it is still correct
// (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL
// driver used as the ES backend. AFTER the link either way, because it needs the
// driver's linked interface.
ReseedShaderStorageBlockBindings(m_backendProgramId, src);
m_isInitialized = true;
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
}
// P5e (pg), CONTRACT-P5E §5.5: THE BUILD THAT NAMES NO CLIENT MEMORY. Everything it
// reads is the record: the archive create_shader_state carried, the three binding tails
// set_program_bindings carried, and the descriptor. Nothing here resolves a frontend
// identity, probes the client allocator or holds a SharedPtr to a frontend object - the
// three absences rule F is made of.
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const auto* record = PipeShaderCsoRecordForHandle(cso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record, so no driver "
"program can be built for it",
cso.Slot, cso.Gen);
return;
}
// THE ARCHIVE IS THE RECORD'S OR THE BUILD DOES NOT HAPPEN. A record with no archive
// is one the transport arm never filled - a monolith create that reached a handle
// arm - and building from an empty reflection would produce a program with no
// uniforms and no modules, i.e. a black screen with no marker anywhere. The
// create's own trip wire already refuses a record that declares no blobs AND
// carries no artefacts (PipeApply.cpp); this is the same statement one level later,
// where the twin can name the handle.
if (!record->Archive) {
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} carries no server-owned archive; its "
"create_shader_state was emitted by the monolith arm and this is "
"the handle arm, so there is nothing to build from",
cso.Slot, cso.Gen);
m_backendProgramUsable = false;
return;
}
SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record));
// The two server-owned keys the draw path's clean condition reads instead of
// GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a
// glUniform1i on an IMAGE uniform is baked into the generated ESSL rather than
// re-issued per draw - the record that carries it must therefore force a rebuild,
// exactly as m_imageUnitVersion used to.
m_syncedShaderCsoSerial = record->Serial;
m_syncedBindingsSerial = record->BindingsSerial;
}
namespace {
// The GL name of the array element that lives at `location`, given the reflection
// name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but
// one location per ELEMENT, so a caller walking locations sees the same name
// repeatedly; this turns it back into "goku[k]". Anything that is not an array
// (or whose base location cannot be resolved) comes back unchanged, so the only
// behaviour that moves is the array case.
String SubscriptUniformNameForElement(const ProgramBuildSource& program, const String& name,
Uint location) {
if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name;
const Int base = program.GetUniformLocation(name);
if (base < 0 || static_cast<Uint>(base) > location) return name;
const Uint element = location - static_cast<Uint>(base);
if (element == 0) return name;
return name.substr(0, name.size() - 3) + "[" + std::to_string(element) + "]";
}
} // namespace
// Resolves every name-based resource lookup once per link so the per-draw path
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
// glGetUniformLocation string queries; block-to-binding-point assignments are
// program state and only need to be established here.
void BackendProgramObjectImpl::CacheResourceLocations(const ProgramBuildSource& src) {
m_globalUboBackendBlockIndex = -1;
m_globalUboBackendBlockSize = 0;
m_lastUploadedGlobalUboVersion = ~0u;
m_globalUboRingAllocation = {};
if (src.GetUBOSize() > 0) {
const Uint blockIndex =
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
if (blockIndex != GL_INVALID_INDEX) {
m_globalUboBackendBlockIndex = static_cast<Int>(blockIndex);
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, blockIndex, 0);
// Ring bindings are ranges and must span the block as the backend
// compiled it (its std140 padding may exceed the frontend's
// SPIR-V-reflected size).
if (g_GLESFuncs.glGetActiveUniformBlockiv) {
GLint blockDataSize = 0;
g_GLESFuncs.glGetActiveUniformBlockiv(m_backendProgramId, blockIndex,
GL_UNIFORM_BLOCK_DATA_SIZE, &blockDataSize);
m_globalUboBackendBlockSize = static_cast<Int>(blockDataSize);
}
} else {
MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.",
src.GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
}
}
const Int uboCount = src.GetActiveUniformBlocksCount();
m_uniformBlockBackendIndices.assign(static_cast<SizeT>(std::max(uboCount, 0)), -1);
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
for (Int i = 0; i < uboCount; ++i) {
++lastUBOBinding;
const auto& name = src.GetUniformBlockName(static_cast<Uint>(i));
const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str());
if (backendBlkIdx == GL_INVALID_INDEX) {
// Either eliminated as unused, or an SSBO block (frontend reflection
// lists those among uniform blocks); SSBO bindings are baked into the ESSL.
continue;
}
m_uniformBlockBackendIndices[static_cast<SizeT>(i)] = static_cast<Int>(backendBlkIdx);
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding);
MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u", src.GetExternalIndex(),
m_backendProgramId, i, name.c_str(), backendBlkIdx, lastUBOBinding);
}
m_samplerUniformBindings.clear();
const Uint maxUniformLoc = src.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = src.GetUniformName(loc);
if (name.empty()) continue;
const GLenum uniformType = src.GetUniformType(loc);
if (IsImageUniformType(uniformType)) {
// ES image units come exclusively from the layout(binding=N) qualifier
// (preserved in the transpiled ESSL); glUniform1i on an image uniform
// is an INVALID_OPERATION.
continue;
}
// Reflection names an array uniform after its FIRST element ("goku[0]") at
// every location the array spans, so asking the driver for that one name
// once per location hands back the same backend location N times. The
// per-draw pass then issues N glUniform1i calls against it and only the
// last element's unit survives - "layout(binding = 1) uniform sampler2D
// goku[7]" ended up with goku[0] on unit 7 and goku[1..6] still on 0.
// Address each element by its own name instead; the frontend already
// reserves one location per element, so the element index is the distance
// from the array's base location.
const String elementName = SubscriptUniformNameForElement(src, name, loc);
const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str());
if (backendLoc < 0) continue;
SamplerUniformBinding binding;
binding.frontendLocation = loc;
binding.backendLocation = backendLoc;
binding.uniformType = uniformType;
binding.lastAssignedUnit = -1;
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation(
m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str());
binding.lastAssignedLodBias = 0.0f;
// Present only for the unsigned multisample samplers ClampUnsignedMultisampleFetches
// rewrote, named the same way.
if (uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE ||
uniformType == GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY) {
binding.msSampleMaxLocation = g_GLESFuncs.glGetUniformLocation(
m_backendProgramId, (String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + elementName).c_str());
}
binding.lastAssignedMsSampleMax = 0;
m_samplerUniformBindings.push_back(binding);
}
}
void BackendProgramObjectImpl::Use() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// glUseProgram on a program that did not link is an INVALID_OPERATION and
// leaves the *previous* program current, so the draw would silently render
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
// test case's alpha that way once a sampler2DRect stage failed to
// transpile). Bind nothing instead: the draw is then a visible no-op.
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
// ...unless SyncToBackend relinked this program since the last bind, in which case
// the id matching proves nothing about the executable behind it.
if (g_lastUsedBackendProgramId == programToBind && !m_rebindAfterRelink) {
return;
}
m_rebindAfterRelink = false;
if (!m_backendProgramUsable) {
// Every draw made with this program renders nothing and raises no GL error, so
// without this line the only symptom is a framebuffer that kept its clear
// colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an
// app alternating a healthy and a broken program would otherwise log every
// single draw. Parked at MGLOG_I until the level ordering was fixed.
MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, "
"compile or link); binding program 0 - draws with it will render nothing",
m_frontendProgramId);
}
MGLOG_D("Using program %u", programToBind);
g_GLESFuncs.glUseProgram(programToBind);
g_lastUsedBackendProgramId = programToBind;
}
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
if (m_baseInstanceUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
}
// A direct value disables the indirect-command-buffer read.
SetBaseInstanceWordIndex(-1);
}
// The uniform is written one-based so that its GLSL initial value, zero, already reads
// as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms.
void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const {
if (m_baseInstanceWordIndexUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, wordIndex < 0 ? 0 : wordIndex + 1);
}
}
void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const {
if (m_baseVertexUniformLocation >= 0) {
g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex);
}
}
void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const {
if (m_drawIdUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast<GLint>(drawId));
}
void BackendProgramObjectImpl::SetViewportPassMask(Uint32 indexMask) const {
if (m_viewportPassMaskUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_viewportPassMaskUniformLocation, static_cast<GLint>(indexMask));
}
} // namespace PrgramImpl
} // namespace MobileGL::MG_Backend::DirectGLES