[Feat, Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_IntegrationTest): bake the bound image format into the ESSL a format-less image declaration needs

This commit is contained in:
2026-08-12 18:56:31 -04:00
parent bce34d7fac
commit b7557d6615
16 changed files with 1976 additions and 1 deletions
@@ -2195,7 +2195,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
twin->GetShaderStorageBlockBindingSignature() !=
ComputeShaderStorageBlockBindingSignature(*currentProgram)) {
ComputeShaderStorageBlockBindingSignature(*currentProgram) ||
// A fourth of the same shape, and the reason glBindImageTexture itself does
// nothing: GLSL ES demands a format layout qualifier on an image where desktop
// GLSL lets a writeonly declaration omit one, so a format-less declaration is
// compiled against the format the application BOUND, and a rebind to a
// different one makes what was built wrong. Asked of the twin because only it
// knows which units its own images address - and answered by an empty-vector
// test for every program that declares its formats, which is nearly all of them.
!twin->ImageUnitFormatsStillMatch()) {
twin->SyncToBackend(currentProgram);
}
g_currentDrawFrontendProgram = currentProgram.get();
+202
View File
@@ -4497,6 +4497,161 @@ namespace MobileGL::MG_Backend::DirectGLES {
return signature;
}
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;
return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);
}
// 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;
}
// 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;
}
// 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;
}
}
} // 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 MG_State::GLState::ProgramObject& stateProgramObject) {
ImageFormatBakeInputs inputs;
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = stateProgramObject.GetUniformName(loc);
if (name.empty()) continue;
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
const glslang::TType* type = stateProgramObject.GetUniformTType(loc);
if (type == nullptr) continue;
if (type->getQualifier().hasFormat()) {
// Declared, and therefore left exactly as written - but a non-core spelling
// still needs the extension directive to survive the ES compiler.
if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) {
inputs.needsExtendedImageFormats = true;
}
continue;
}
const Int unit = stateProgramObject.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)) {
// Outside the GLSL ES core set, so the emitted ESSL only compiles with
// GL_NV_image_formats. Without the extension there is no legal spelling at
// all, and baking one would trade a "no format qualifier" compile error for
// an "unsupported format" one - so the image is left format-less. Its unit
// stays in the key, so a rebind to a core format still rebuilds and works.
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
"core cannot spell and this driver has no GL_NV_image_formats for.",
name.c_str(), unit, boundFormat);
continue;
}
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;
}
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);
}
return inputs;
}
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;
}
void BackendProgramObjectImpl::SyncToBackend(
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
#ifdef TRACY_ENABLE
@@ -4537,6 +4692,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
// 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(*stateProgramObject);
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(), stateProgramObject->GetExternalIndex());
}
// Detach all existing shaders
GLint attachedCount = 0;
@@ -4719,6 +4886,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &arrayImageSpirv;
}
// 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) &&
!imageFormatSpirv.empty()) {
effectiveSpirv = &imageFormatSpirv;
}
// 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
@@ -4786,8 +4972,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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);
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
// 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);
// 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
+62
View File
@@ -1122,6 +1122,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stale as one built before a relink - while the sampler half, which really is
// re-issued per draw, needs nothing of the sort.
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
// resolve to are still the ones its ESSL was generated against.
//
// A fourth condition of the same family as the three above, and the only one that
// reads live state rather than a program-side counter, because that is where the
// dependency actually is. GLSL ES requires a format layout qualifier on every image
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
// qualifier is whatever glBindImageTexture named - so a declaration with no format
// is compiled against the BINDING, and a rebind to a different format makes the
// built program wrong. Keyed on the units the program's own images address (cached
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
// version above and forces a re-sync anyway), so the cost on a program with no
// format-less image - which is all but a handful - is one empty-vector test.
//
// Deliberately NOT reached from glBindImageTexture: that entry point must never
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
// state and this comparison notices at the next Prepare, which is also what makes
// an image first bound AFTER link work.
Bool ImageUnitFormatsStillMatch() const;
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
// image-unit state. 0 when the program has no format-less image uniform.
Uint64 ComputeImageUnitFormatSignature() const;
private:
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
@@ -1155,6 +1177,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
Uint32 m_syncedLinkVersion = ~0u;
Uint32 m_syncedImageUnitVersion = ~0u;
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
// that declares a format on all of its images, which is the overwhelming majority -
// and what keeps the per-draw comparison free for them.
Vector<Int> m_formatlessImageUnits;
Uint64 m_imageUnitFormatSignature = 0;
SamplerPassMemo m_samplerPassMemo;
};
@@ -1198,6 +1226,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already has costs nothing. 0 when nothing was ever rebound.
Uint64 ComputeShaderStorageBlockBindingSignature(
const MG_State::GLState::ProgramObject& stateProgramObject);
// Everything the image-format bake needs from one walk of a program's uniform
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
// on it.
struct ImageFormatBakeInputs {
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
// one is authoritative and is never overridden.
UnorderedMap<String, Uint> glFormatByUniformName;
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
// write into the emitted declaration instead. Disjoint from the map above by
// construction: a format is baked into the module or completed in the text, never
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
UnorderedMap<String, String> esslFormatQualifierByUniformName;
// Units those uniforms address, kept so the draw path can re-read their formats
// without walking the reflection again.
Vector<Int> units;
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
// image uniform, which is all but a handful.
Uint64 signature = 0;
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
// declaration carries one qualifier, so there is nothing correct to bake and they
// are dropped from the map above. Kept for diagnostics.
Vector<String> conflictedNames;
// Some format in play - declared or baked - is outside the GLSL ES core image
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
Bool needsExtendedImageFormats = false;
};
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject);
} // namespace PrgramImpl
namespace SamplerImpl {
+86
View File
@@ -535,6 +535,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String RequestExtendedImageFormats(String glslCode, Bool needed) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
// a format its core language does not know. GL_NV_image_formats is the only thing
// that adds them, and it has to be requested by name.
//
// The caller decides `needed`: it knows which formats are in play (from the uniform
// reflection and the image-unit bindings) and whether the driver advertises the
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
// this must never be emitted speculatively.
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
static constexpr const char* kExtName = "GL_NV_image_formats";
if (!needed || glslCode.find(kExtName) != String::npos) {
return glslCode;
}
// After the #version line, which must stay first. Everything else about the header is
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
// directive still finds whichever one that is.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
return glslCode;
}
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
// reason: one line, one image uniform, the name in group 3.
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*;)");
// Every image format spelling GLSL has, so a declaration that already carries one is
// recognised whatever it says - the caller's map is consulted only for declarations
// with NO format, never to override a written one.
static const std::regex existingFormatRegex(
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
const SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
std::smatch match;
if (std::regex_search(line, match, imageDeclRegex)) {
const String name = match[3].str();
const auto formatIt = esslFormatByUniformName.find(name);
const String layoutContents = match[2].matched ? match[2].str() : String();
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
!std::regex_search(layoutContents, existingFormatRegex)) {
if (match[1].matched) {
const SizeT layoutOpen = line.find('(', match.position(1));
line.insert(layoutOpen + 1, formatIt->second + ", ");
} else {
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
}
}
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
String RemoveLayoutBinding(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
+20
View File
@@ -137,6 +137,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own name.