Merge branch "feat/cts-image-format-qualifier" into dev

This commit is contained in:
2026-08-12 19:23:51 -04:00
16 changed files with 2012 additions and 1 deletions
+1
View File
@@ -285,6 +285,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
@@ -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.
@@ -70,6 +70,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
@@ -0,0 +1,314 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.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
//
// Scenario - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
//
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
//
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
// images have to define layout format", which fails the whole program. That is what took the
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
//
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
// internal format to belong to one format class - so the format is not knowable when the shader
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
// generates and keys that program on the (unit, format) pairs it baked
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
//
// Three separate things follow from "the program is built against live binding state", and each
// one is a case below:
//
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
// compiled against the old format (RebindToADifferentFormatRebuilds);
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
//
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
// at descriptor time - so every case here runs on both backends and must agree, which is what
// makes the ES-only machinery falsifiable rather than merely exercised.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
constexpr GLint kImageUnit = 1;
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
const char* kStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)";
class ImageFormatQualifierScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
m_programs.clear();
m_textures.clear();
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
}
GLuint MakeComputeProgram(const std::string& source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
return 0;
}
return program;
}
GLuint MakeTexture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
return 0;
}
// Seeded to a value no dispatch writes, so "the store never happened" and "the
// store wrote the right thing" cannot be confused.
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
zeros.data());
while (glGetError() != GL_NO_ERROR) {
}
return texture;
}
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
// than through a second image uniform: a defect in the format bake would be shared by
// a reader declared the same way and could cancel itself out.
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
const bool rgba = internalFormat == GL_RGBA32UI;
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
0xFFFFFFFFu);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
texels.data());
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
return 0xFFFFFFFFu;
}
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
}
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
internalFormat);
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0) << "the image uniform was not reflected";
glUniform1i(location, kImageUnit);
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// The defect itself. Without the bake the ES driver refuses the program outright and the
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
// like, and why the seed is a value no store writes.
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << " of a format-less writeonly image did not take the store";
}
}
// The rebuild key. The SAME program is dispatched twice with a different format bound to
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
//
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
// program was built with no format at all and never compiled. Both are kept: this one is
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint first = MakeTexture(GL_R32UI);
const GLuint second = MakeTexture(GL_RGBA32UI);
if (program == 0 || first == 0 || second == 0) return;
DispatchStore(program, first, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "the first format must work before the rebind can be blamed for anything";
}
DispatchStore(program, second, GL_RGBA32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
}
// ...and back, so the rebuild is not a one-way door: returning to a format the
// program was once built against must build for it again, not resurrect a cache row.
const GLuint third = MakeTexture(GL_R32UI);
if (third == 0) return;
DispatchStore(program, third, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": going back to the first format did not rebuild";
}
}
// Nothing is bound to the unit when the program links, so whatever the first build sees
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
// build - it is an entry point, and building there is the constraint
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
// build attempted from the entry point before the state settles.
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
if (program == 0) return;
// Use it once with NOTHING bound to the unit, which is what makes the backend build
// against an empty binding. The dispatch writes nowhere and must not error.
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0);
glUniform1i(location, kImageUnit);
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
glUseProgram(0);
const GLuint texture = MakeTexture(GL_R32UI);
if (texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the first bind after the link did not reach the shader";
}
}
// A DECLARED format is authoritative and the bake must never touch it - including when
// the texture behind the unit has a different (but class-compatible) internal format,
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
// that would go wrong while every other one stayed green.
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)");
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": a declared format stopped working";
}
}
} // namespace
} // namespace MGITest
@@ -16,9 +16,11 @@
#include <MG_Backend/DirectGLES/Utils.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
namespace {
@@ -435,3 +437,116 @@ void main() { tes_gs_coord = tcs_tes_coord[0]; }
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
}
// --- image format qualifier completion ---------------------------------------------------------
//
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
// with GL_R8UI bound to its unit.
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
const String source = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
// exactly the reason this pass exists.
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
const String source = R"(#version 320 es
uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
// puts a declared image in the map, and the pass must not depend on that being true.
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
const String source = R"(#version 320 es
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_EQ(out, source) << out;
}
// Only the named uniform. A second image in the same shader - format-less because the pass
// declined it, or because its unit holds nothing - must be left exactly as it is.
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
const String source = R"(#version 320 es
layout(binding = 0) uniform writeonly highp uimage2D named;
layout(binding = 1) uniform writeonly highp uimage2D other;
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
}
// The format the pass writes has to survive the two passes that run after it, or nothing was
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
const String source = R"(#version 320 es
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
out = SplitReadWriteImageUniforms(out);
out = RemoveLayoutBinding(out);
EXPECT_TRUE(Contains(out, "r8ui")) << out;
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
}
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
const String withImage = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
const String withoutImage = R"(#version 320 es
layout(location = 0) out highp vec4 mg_FragColor;
void main() { mg_FragColor = vec4(1.0); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
}
// --- GL_NV_image_formats directive --------------------------------------------------------------
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
}
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
// compile error, so the caller's "not needed" answer has to be honoured exactly.
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
const String source = R"(#version 320 es
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
}
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_NV_image_formats : require
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
}
@@ -3689,3 +3689,316 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
<< "declining means the 1D-array type is still there for the driver to reject";
}
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
// requires one of every image, and Adreno says so as "all images have to define layout format",
// losing the whole program. The only correct qualifier to substitute is the format the
// application passed to glBindImageTexture for that unit, so the transpile bakes it in.
namespace {
Uint CountSpirvOpcode(const String& disassembly, const String& opcode) {
Uint count = 0;
SizeT offset = 0;
const String needle = opcode + " ";
while ((offset = disassembly.find(needle, offset)) != String::npos) {
count += 1;
offset += needle.size();
}
return count;
}
constexpr Uint kGlR32ui = 0x8236;
constexpr Uint kGlRgba32ui = 0x8D70;
constexpr Uint kGlR8ui = 0x8232;
constexpr Uint kGlR32f = 0x822E;
} // namespace
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
// writeonly image, and a bind of a concrete format to the unit it addresses. (The DEPTH half of
// that case binds GL_R32F; the stencil half's GL_R8UI is one SPIRV-Cross will not print and takes
// the text route instead - see the test below.)
TEST_F(ProgramUtilTest, BakeImageFormatsGivesAFormatlessImageTheFormatBoundToItsUnit) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, 0u, 0u, 0u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
<< DisassembleSpirv(spirv);
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked module must stay validator-clean:\n"
<< DisassembleSpirv(baked);
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos)
<< "the bound format must reach the declaration as a layout qualifier:\n" << essl;
EXPECT_NE(essl.find("writeonly"), String::npos)
<< "the access qualifier the declaration already had must survive:\n" << essl;
}
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
// into the module would trade a missing qualifier for a missing shader. They are left format-less
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
// which the stencil half of the packed_depth_stencil case binds, is one of them.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
using namespace MG_Util::ShaderTranspiler;
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
// ...and the stage still transpiles, which is the whole point of declining.
EXPECT_FALSE(DecompileToEssl(baked).empty());
}
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
// texture's internal format to be in the same class, but the qualifier is what the shader is
// specified to read the memory as, and a bake that overrode it would change what the shader does.
TEST_F(ProgramUtilTest, BakeImageFormatsNeverOverridesADeclaredFormat) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
layout (binding = 0, rgba32ui) writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
Vector<Uint32> baked;
// Even asked to, with a format of the right component class.
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
}
// Review finding. Every use has to be one the retype can carry end to end, and the decision has
// to be made BEFORE anything is mutated - a half-retyped module is not something a later decline
// could undo. An image handed to a FUNCTION is the shape that reaches SPIRV-Cross intact (nothing
// in the ESSL chain inlines), and its OpFunctionCall is a use this pass does not follow.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAnImagePassedToAFunction) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void writeIt(writeonly uimage2D img) { imageStore(img, ivec2(0), uvec4(1u)); }
void main() { writeIt(uni_image); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
"not partly rewritten:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
}
// spirv-val requires the Image Format's component class to agree with the OpTypeImage's Sampled
// Type. Binding a uint format to a float image is an application error GL leaves undefined;
// baking it would turn that into an INVALID module, which is strictly worse than the compile
// error the shader already has, so the image is left format-less.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAFormatOfTheWrongComponentClass) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform image2D uni_image;
void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
// ...and the same image with a float bind format is baked, so the decline above is about the
// class and not about the pass refusing float images.
Vector<Uint32> bakedFloat;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
}
// Two format-less images of the same type share ONE OpTypeImage. Giving them different formats
// therefore cannot be an in-place edit of that type - each needs its own declaration, and the
// variable, the loads and (for arrays) the access chains all have to follow.
TEST_F(ProgramUtilTest, BakeImageFormatsSplitsATypeTwoImagesShareWhenTheirFormatsDiffer) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgA;
writeonly uniform uimage2D imgB;
void main() {
imageStore(imgA, ivec2(0), uvec4(1u));
imageStore(imgB, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
<< DisassembleSpirv(baked);
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos) << essl;
EXPECT_NE(essl.find("rgba32ui"), String::npos) << essl;
}
// The mirror of the split: when the module ALREADY declares the type the bake wants, the two must
// be JOINED, not duplicated. SPIR-V forbids two identical non-aggregate type declarations, and
// that is exactly the defect an earlier image pass shipped and a reviewer caught.
TEST_F(ProgramUtilTest, BakeImageFormatsJoinsATypeTheModuleAlreadyDeclares) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D formatless;
layout (binding = 1, r32ui) writeonly uniform uimage2D declared;
void main() {
imageStore(formatless, ivec2(0), uvec4(1u));
imageStore(declared, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(CountSpirvOpcode(DisassembleSpirv(baked), "OpTypeImage"), 1u)
<< "the two identical image types must be the same declaration:\n" << DisassembleSpirv(baked);
}
// An ARRAY of format-less images: the variable's type is a pointer to an array, every use goes
// through an OpAccessChain, and all three levels have to be rebuilt for the load to still type-check.
TEST_F(ProgramUtilTest, BakeImageFormatsRetypesAnArrayOfFormatlessImagesThroughItsAccessChains) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgs[2];
void main() {
for (int i = 0; i < 2; ++i) imageStore(imgs[i], ivec2(0), uvec4(uint(i)));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the array and pointer types above the image must have been rebuilt too:\n"
<< DisassembleSpirv(baked);
EXPECT_NE(DecompileToEssl(baked).find("r32ui"), String::npos);
}
// A SAMPLED image's format operand is Unknown in every GLSL dialect and has no qualifier to bake;
// only storage images (Sampled == 2) are in scope.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesSampledImagesAlone) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
uniform usampler2D uni_sampler;
out uvec4 fragColor;
in vec2 vUv;
void main() { fragColor = texture(uni_sampler, vUv); }
)",
GL_FRAGMENT_SHADER);
ASSERT_FALSE(spirv.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
}
// The core/extended split the emitted ESSL depends on: GLSL ES has thirteen image formats, and a
// bind format outside them only compiles with GL_NV_image_formats - which the backend must not
// request on a driver that does not advertise it.
TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
using namespace MG_Util::ShaderTranspiler;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlRgba32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32f));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8058 /*GL_RGBA8*/));
// The stencil half of KHR-GL4x.packed_depth_stencil.stencil_texturing binds this one, and it
// is NOT core - the whole reason the directive machinery exists.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR8ui));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x822D /*GL_R16F*/));
// Not an image format at all.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
}
@@ -912,6 +912,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
if (std::strcmp(extension, "GL_NV_image_formats") == 0) {
caps.SupportsExtendedImageFormats = true;
}
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
caps.SupportsShaderMultisampleInterpolation = true;
}
@@ -1138,6 +1138,14 @@ namespace MobileGL {
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
Bool SupportsNoperspectiveInterpolation = false;
// GL_NV_image_formats is present: the driver accepts the image format qualifiers GL
// has and GLSL ES core does not (the one- and two-channel formats, the 16-bit and
// snorm ones - r8ui, rg16f, rgba16 and the rest of GL table 8.26). GLSL ES core has
// only thirteen, so without this an image whose bound format is outside that set has
// no legal spelling in the generated ESSL at all, and the directive must not be
// emitted either - `#extension` on a name the driver does not advertise is itself a
// compile error.
Bool SupportsExtendedImageFormats = false;
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
// interpolateAtOffset and the three fragment-offset limit queries.
Bool SupportsShaderMultisampleInterpolation = false;
@@ -27,6 +27,7 @@
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/BakeImageFormatsPass.h"
#include "SpirvPasses/PrivateToEntryLocalPass.h"
#include "SpirvPasses/StripUniformLocationsPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -686,6 +687,35 @@ namespace MobileGL {
outputBinary);
}
bool ShaderCompiler::BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
if (glFormatByName.empty()) return false;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(BakeImageFormatsPass::CreateBakeImageFormatsPass(glFormatByName));
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary);
}
bool ShaderCompiler::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
return BakeImageFormatsPass::DeclaresFormatlessStorageImage(binary);
}
bool ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat) {
return BakeImageFormatsPass::IsCoreEsslImageFormat(
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
}
String ShaderCompiler::EsslImageFormatSpelling(Uint glInternalFormat) {
return BakeImageFormatsPass::EsslSpellingOfGLInternalFormat(glInternalFormat);
}
bool ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat) {
return BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -97,6 +97,32 @@ namespace MobileGL {
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Gives each format-less storage image the format bound to its image unit, so
// the emitted ESSL can carry the format layout qualifier GLSL ES requires of
// every image and desktop GLSL lets a writeonly declaration omit. `glFormatByName`
// maps uniform name to the glBindImageTexture format of the unit it addresses.
// DirectGLES transpile path only - Vulkan takes an Unknown-format storage image
// natively. See BakeImageFormatsPass for what it declines and why.
static bool BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
const UnorderedMap<String, Uint>& glFormatByName,
Vector<uint32_t>& outputBinary);
// Whether the module declares a storage image with no format qualifier at all,
// i.e. whether BakeImageFormatsForEssl could change anything. One module parse,
// so the ~every shader that declares none pays no optimizer run.
static bool DeclaresFormatlessStorageImage(const Vector<Uint32>& binary);
// Whether the GL internal format's image-format spelling is one GLSL ES has in
// core. False both for a format ES only reaches through GL_NV_image_formats and
// for one with no image-format spelling at all, so a caller that has to decide
// whether to emit the extension directive can ask this one question.
static bool GLInternalFormatIsCoreEsslImageFormat(Uint glInternalFormat);
// The ESSL layout-qualifier spelling of a GL internal format ("r8ui", "rgba32f"),
// empty when the format has no image-format spelling at all.
static String EsslImageFormatSpelling(Uint glInternalFormat);
// Whether SPIRV-Cross will print that format when it targets ESSL. It throws on
// the ones it calls desktop-only - taking the whole stage with it - so a caller
// must not ask BakeImageFormatsForEssl for those, and completes them in the
// emitted text instead.
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
@@ -0,0 +1,706 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.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 "BakeImageFormatsPass.h"
#include "spirv.hpp"
#include "source/opt/build_module.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <map>
#include <memory>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
// 5 Sampled, 6 Format, (7 Access Qualifier - Kernel only, never present here).
constexpr uint32_t kImageSampledTypeOperand = 0;
constexpr uint32_t kImageSampledOperand = 5;
constexpr uint32_t kImageFormatOperand = 6;
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
// sampler. The only kind that has a format qualifier in any GLSL dialect.
constexpr uint32_t kSampledStorageImage = 2;
// OpTypePointer in-operands: 0 storage class, 1 pointee.
constexpr uint32_t kPointerStorageClassOperand = 0;
constexpr uint32_t kPointerPointeeOperand = 1;
// OpTypeArray in-operands: 0 element type, 1 length.
constexpr uint32_t kArrayElementOperand = 0;
bool IsFormatlessStorageImageType(const Instruction* type) {
return type != nullptr && type->opcode() == spv::Op::OpTypeImage &&
type->GetSingleWordInOperand(kImageSampledOperand) == kSampledStorageImage &&
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand)) ==
spv::ImageFormat::Unknown;
}
// The three component classes a format layout qualifier can have. spirv-val
// requires the Image Format's class to agree with the OpTypeImage's Sampled
// Type, so a bake that disagrees would produce an invalid module rather than a
// merely wrong one.
enum class ComponentClass { Float, SignedInt, UnsignedInt, None };
ComponentClass ClassOfImageFormat(spv::ImageFormat format) {
switch (format) {
case spv::ImageFormat::Rgba32f:
case spv::ImageFormat::Rgba16f:
case spv::ImageFormat::R32f:
case spv::ImageFormat::Rgba8:
case spv::ImageFormat::Rgba8Snorm:
case spv::ImageFormat::Rg32f:
case spv::ImageFormat::Rg16f:
case spv::ImageFormat::R11fG11fB10f:
case spv::ImageFormat::R16f:
case spv::ImageFormat::Rgba16:
case spv::ImageFormat::Rgb10A2:
case spv::ImageFormat::Rg16:
case spv::ImageFormat::Rg8:
case spv::ImageFormat::R16:
case spv::ImageFormat::R8:
case spv::ImageFormat::Rgba16Snorm:
case spv::ImageFormat::Rg16Snorm:
case spv::ImageFormat::Rg8Snorm:
case spv::ImageFormat::R16Snorm:
case spv::ImageFormat::R8Snorm:
return ComponentClass::Float;
case spv::ImageFormat::Rgba32i:
case spv::ImageFormat::Rgba16i:
case spv::ImageFormat::Rgba8i:
case spv::ImageFormat::R32i:
case spv::ImageFormat::Rg32i:
case spv::ImageFormat::Rg16i:
case spv::ImageFormat::Rg8i:
case spv::ImageFormat::R16i:
case spv::ImageFormat::R8i:
return ComponentClass::SignedInt;
case spv::ImageFormat::Rgba32ui:
case spv::ImageFormat::Rgba16ui:
case spv::ImageFormat::Rgba8ui:
case spv::ImageFormat::R32ui:
case spv::ImageFormat::Rgb10a2ui:
case spv::ImageFormat::Rg32ui:
case spv::ImageFormat::Rg16ui:
case spv::ImageFormat::Rg8ui:
case spv::ImageFormat::R16ui:
case spv::ImageFormat::R8ui:
return ComponentClass::UnsignedInt;
default:
return ComponentClass::None;
}
}
ComponentClass ClassOfSampledType(IRContext* context, uint32_t sampledTypeId) {
const Instruction* sampledType = context->get_def_use_mgr()->GetDef(sampledTypeId);
if (sampledType == nullptr) return ComponentClass::None;
if (sampledType->opcode() == spv::Op::OpTypeFloat) return ComponentClass::Float;
if (sampledType->opcode() == spv::Op::OpTypeInt) {
// OpTypeInt in-operands: 0 width, 1 signedness.
return sampledType->GetSingleWordInOperand(1) != 0 ? ComponentClass::SignedInt
: ComponentClass::UnsignedInt;
}
return ComponentClass::None;
}
// The image type at the end of a UniformConstant variable's type chain, plus the
// links along the way. Anything that is not `pointer -> [array ->] image` comes
// back with a null image and is left alone.
struct ImageTypeChain {
Instruction* pointerType = nullptr; // the variable's own result type
Instruction* arrayType = nullptr; // null when the variable is a single image
Instruction* imageType = nullptr;
};
ImageTypeChain ResolveImageTypeChain(IRContext* context, const Instruction& variable) {
ImageTypeChain chain;
auto* defUseMgr = context->get_def_use_mgr();
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
return chain;
}
chain.pointerType = pointerType;
Instruction* pointee = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(kPointerPointeeOperand));
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) {
chain.arrayType = pointee;
pointee = defUseMgr->GetDef(pointee->GetSingleWordInOperand(kArrayElementOperand));
}
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeImage) {
chain.imageType = pointee;
}
return chain;
}
// Instructions that consume an image VALUE (the result of an OpLoad) and need no
// result-type change of their own: the texel type they yield is independent of
// the format operand.
bool ConsumesImageValueWithoutRetyping(spv::Op opcode) {
switch (opcode) {
case spv::Op::OpImageWrite:
case spv::Op::OpImageRead:
case spv::Op::OpImageSparseRead:
case spv::Op::OpImageQuerySize:
case spv::Op::OpImageQuerySizeLod:
case spv::Op::OpImageQuerySamples:
case spv::Op::OpImageQueryLevels:
case spv::Op::OpImageQueryFormat:
case spv::Op::OpImageQueryOrder:
return true;
default:
return false;
}
}
// Debug/annotation instructions name an id without depending on its type.
bool IsTypeAgnosticReference(spv::Op opcode) {
switch (opcode) {
case spv::Op::OpName:
case spv::Op::OpMemberName:
case spv::Op::OpDecorate:
case spv::Op::OpDecorateId:
case spv::Op::OpDecorateString:
case spv::Op::OpMemberDecorate:
case spv::Op::OpEntryPoint:
return true;
default:
return false;
}
}
} // namespace
Uint32 BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(Uint glInternalFormat) {
switch (glInternalFormat) {
// The GL 4.2 image format table (core spec table 8.26), in its order. Written as
// literals rather than through the GL headers because this lives in MG_Util,
// which the GL frontend's enums do not reach.
case 0x8814: /*GL_RGBA32F*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32f);
case 0x881A: /*GL_RGBA16F*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16f);
case 0x8230: /*GL_RG32F*/ return static_cast<Uint32>(spv::ImageFormat::Rg32f);
case 0x822F: /*GL_RG16F*/ return static_cast<Uint32>(spv::ImageFormat::Rg16f);
case 0x8C3A: /*GL_R11F_G11F_B10F*/ return static_cast<Uint32>(spv::ImageFormat::R11fG11fB10f);
case 0x822E: /*GL_R32F*/ return static_cast<Uint32>(spv::ImageFormat::R32f);
case 0x822D: /*GL_R16F*/ return static_cast<Uint32>(spv::ImageFormat::R16f);
case 0x8D70: /*GL_RGBA32UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32ui);
case 0x8D76: /*GL_RGBA16UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16ui);
case 0x8D7C: /*GL_RGBA8UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8ui);
case 0x906F: /*GL_RGB10_A2UI*/ return static_cast<Uint32>(spv::ImageFormat::Rgb10a2ui);
case 0x823C: /*GL_RG32UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg32ui);
case 0x823A: /*GL_RG16UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg16ui);
case 0x8238: /*GL_RG8UI*/ return static_cast<Uint32>(spv::ImageFormat::Rg8ui);
case 0x8236: /*GL_R32UI*/ return static_cast<Uint32>(spv::ImageFormat::R32ui);
case 0x8234: /*GL_R16UI*/ return static_cast<Uint32>(spv::ImageFormat::R16ui);
case 0x8232: /*GL_R8UI*/ return static_cast<Uint32>(spv::ImageFormat::R8ui);
case 0x8D82: /*GL_RGBA32I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba32i);
case 0x8D88: /*GL_RGBA16I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16i);
case 0x8D8E: /*GL_RGBA8I*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8i);
case 0x823B: /*GL_RG32I*/ return static_cast<Uint32>(spv::ImageFormat::Rg32i);
case 0x8239: /*GL_RG16I*/ return static_cast<Uint32>(spv::ImageFormat::Rg16i);
case 0x8237: /*GL_RG8I*/ return static_cast<Uint32>(spv::ImageFormat::Rg8i);
case 0x8235: /*GL_R32I*/ return static_cast<Uint32>(spv::ImageFormat::R32i);
case 0x8233: /*GL_R16I*/ return static_cast<Uint32>(spv::ImageFormat::R16i);
case 0x8231: /*GL_R8I*/ return static_cast<Uint32>(spv::ImageFormat::R8i);
case 0x8058: /*GL_RGBA8*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8);
case 0x805B: /*GL_RGBA16*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16);
case 0x8059: /*GL_RGB10_A2*/ return static_cast<Uint32>(spv::ImageFormat::Rgb10A2);
case 0x822B: /*GL_RG8*/ return static_cast<Uint32>(spv::ImageFormat::Rg8);
case 0x822C: /*GL_RG16*/ return static_cast<Uint32>(spv::ImageFormat::Rg16);
case 0x8229: /*GL_R8*/ return static_cast<Uint32>(spv::ImageFormat::R8);
case 0x822A: /*GL_R16*/ return static_cast<Uint32>(spv::ImageFormat::R16);
case 0x8F97: /*GL_RGBA8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rgba8Snorm);
case 0x8F9B: /*GL_RGBA16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rgba16Snorm);
case 0x8F95: /*GL_RG8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rg8Snorm);
case 0x8F99: /*GL_RG16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::Rg16Snorm);
case 0x8F94: /*GL_R8_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::R8Snorm);
case 0x8F98: /*GL_R16_SNORM*/ return static_cast<Uint32>(spv::ImageFormat::R16Snorm);
default:
return static_cast<Uint32>(spv::ImageFormat::Unknown);
}
}
bool BakeImageFormatsPass::IsCoreEsslImageFormat(Uint32 spirvImageFormat) {
// GLSL ES 3.1 / 3.2, table "Image Formats". Everything else in the GL table
// exists on ES only through GL_NV_image_formats.
switch (static_cast<spv::ImageFormat>(spirvImageFormat)) {
case spv::ImageFormat::Rgba32f:
case spv::ImageFormat::Rgba16f:
case spv::ImageFormat::R32f:
case spv::ImageFormat::Rgba8:
case spv::ImageFormat::Rgba8Snorm:
case spv::ImageFormat::Rgba32i:
case spv::ImageFormat::Rgba16i:
case spv::ImageFormat::Rgba8i:
case spv::ImageFormat::R32i:
case spv::ImageFormat::Rgba32ui:
case spv::ImageFormat::Rgba16ui:
case spv::ImageFormat::Rgba8ui:
case spv::ImageFormat::R32ui:
return true;
default:
return false;
}
}
bool BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(Uint32 spirvImageFormat) {
// Mirrors SPIRV-Cross's Compiler::is_desktop_only_format (spirv_cross.cpp), which
// CompilerGLSL::format_to_glsl consults before printing: for ESSL output it throws
// on these instead of emitting a token, and the throw takes the whole stage with
// it. NOT the same set as "outside GLSL ES core" - SPIRV-Cross is happy to print
// rg32f, rg16f and the rest of the two-channel 16/32-bit formats for ES, which
// core ES does not have either. Kept as its own list for that reason: the
// question here is what the emitter will do, not what the language allows.
switch (static_cast<spv::ImageFormat>(spirvImageFormat)) {
case spv::ImageFormat::R11fG11fB10f:
case spv::ImageFormat::R16f:
case spv::ImageFormat::Rgb10A2:
case spv::ImageFormat::R8:
case spv::ImageFormat::Rg8:
case spv::ImageFormat::R16:
case spv::ImageFormat::Rg16:
case spv::ImageFormat::Rgba16:
case spv::ImageFormat::R16Snorm:
case spv::ImageFormat::Rg16Snorm:
case spv::ImageFormat::Rgba16Snorm:
case spv::ImageFormat::R8Snorm:
case spv::ImageFormat::Rg8Snorm:
case spv::ImageFormat::R8ui:
case spv::ImageFormat::Rg8ui:
case spv::ImageFormat::R16ui:
case spv::ImageFormat::Rgb10a2ui:
case spv::ImageFormat::R8i:
case spv::ImageFormat::Rg8i:
case spv::ImageFormat::R16i:
return false;
case spv::ImageFormat::Unknown:
return false;
default:
return true;
}
}
String BakeImageFormatsPass::EsslSpellingOfGLInternalFormat(Uint glInternalFormat) {
switch (static_cast<spv::ImageFormat>(SpirvImageFormatFromGLInternalFormat(glInternalFormat))) {
case spv::ImageFormat::Rgba32f: return "rgba32f";
case spv::ImageFormat::Rgba16f: return "rgba16f";
case spv::ImageFormat::R32f: return "r32f";
case spv::ImageFormat::Rgba8: return "rgba8";
case spv::ImageFormat::Rgba8Snorm: return "rgba8_snorm";
case spv::ImageFormat::Rg32f: return "rg32f";
case spv::ImageFormat::Rg16f: return "rg16f";
case spv::ImageFormat::R11fG11fB10f: return "r11f_g11f_b10f";
case spv::ImageFormat::R16f: return "r16f";
case spv::ImageFormat::Rgba16: return "rgba16";
case spv::ImageFormat::Rgb10A2: return "rgb10_a2";
case spv::ImageFormat::Rg16: return "rg16";
case spv::ImageFormat::Rg8: return "rg8";
case spv::ImageFormat::R16: return "r16";
case spv::ImageFormat::R8: return "r8";
case spv::ImageFormat::Rgba16Snorm: return "rgba16_snorm";
case spv::ImageFormat::Rg16Snorm: return "rg16_snorm";
case spv::ImageFormat::Rg8Snorm: return "rg8_snorm";
case spv::ImageFormat::R16Snorm: return "r16_snorm";
case spv::ImageFormat::R8Snorm: return "r8_snorm";
case spv::ImageFormat::Rgba32i: return "rgba32i";
case spv::ImageFormat::Rgba16i: return "rgba16i";
case spv::ImageFormat::Rgba8i: return "rgba8i";
case spv::ImageFormat::R32i: return "r32i";
case spv::ImageFormat::Rg32i: return "rg32i";
case spv::ImageFormat::Rg16i: return "rg16i";
case spv::ImageFormat::Rg8i: return "rg8i";
case spv::ImageFormat::R16i: return "r16i";
case spv::ImageFormat::R8i: return "r8i";
case spv::ImageFormat::Rgba32ui: return "rgba32ui";
case spv::ImageFormat::Rgba16ui: return "rgba16ui";
case spv::ImageFormat::Rgba8ui: return "rgba8ui";
case spv::ImageFormat::R32ui: return "r32ui";
case spv::ImageFormat::Rgb10a2ui: return "rgb10_a2ui";
case spv::ImageFormat::Rg32ui: return "rg32ui";
case spv::ImageFormat::Rg16ui: return "rg16ui";
case spv::ImageFormat::Rg8ui: return "rg8ui";
case spv::ImageFormat::R16ui: return "r16ui";
case spv::ImageFormat::R8ui: return "r8ui";
default:
return {};
}
}
bool BakeImageFormatsPass::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
std::unique_ptr<IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
binary.data(), binary.size());
if (!context) {
return false;
}
for (const Instruction& type : context->module()->types_values()) {
if (IsFormatlessStorageImageType(&type)) {
return true;
}
}
return false;
}
spvtools::opt::Pass::Status BakeImageFormatsPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Cheap gate first: no format-less storage image type, nothing this pass can do,
// and the module is handed back byte-identical.
bool hasFormatlessType = false;
for (const Instruction& type : irContext->types_values()) {
if (IsFormatlessStorageImageType(&type)) {
hasFormatlessType = true;
break;
}
}
if (!hasFormatlessType || m_glFormatByName.empty()) {
return Status::SuccessWithoutChange;
}
// Variable name -> OpName target, built once. OpName is how the frontend's
// reflection and this module agree on which uniform is which; SPIRV-Cross
// preserves it, which is also why the emitted ESSL can be matched by name later.
std::map<uint32_t, const Instruction*> nameById;
for (const Instruction& debugInst : irContext->debugs2()) {
if (debugInst.opcode() == spv::Op::OpName) {
nameById.emplace(debugInst.GetSingleWordInOperand(0), &debugInst);
}
}
struct Candidate {
Instruction* variable = nullptr;
ImageTypeChain chain;
spv::ImageFormat format = spv::ImageFormat::Unknown;
// The access chains and loads that reach the image through this variable,
// collected while validating so the mutation half never has to re-walk.
std::vector<Instruction*> accessChains;
std::vector<Instruction*> loads;
};
std::vector<Candidate> candidates;
for (Instruction& global : irContext->types_values()) {
if (global.opcode() != spv::Op::OpVariable) continue;
if (static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
spv::StorageClass::UniformConstant) {
continue;
}
// An initializer would be a second operand, and the variable is moved behind
// its new type below - which would put it in front of that initializer.
// GLSL never gives a UniformConstant image one; refuse rather than reason.
if (global.NumInOperands() > 1) continue;
const ImageTypeChain chain = ResolveImageTypeChain(irContext, global);
if (!IsFormatlessStorageImageType(chain.imageType)) continue;
const auto nameIt = nameById.find(global.result_id());
if (nameIt == nameById.end()) continue;
const String uniformName = nameIt->second->GetInOperand(1).AsString();
const auto formatIt = m_glFormatByName.find(uniformName);
if (formatIt == m_glFormatByName.end()) continue;
const auto format =
static_cast<spv::ImageFormat>(SpirvImageFormatFromGLInternalFormat(formatIt->second));
if (format == spv::ImageFormat::Unknown) continue;
// SPIRV-Cross THROWS rather than prints for the formats it calls
// desktop-only when targeting ESSL, and a throw loses the whole stage - so
// baking one of those would trade a missing qualifier for a missing shader.
// Those formats are completed in the emitted text instead (see
// PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for
// them, which is exactly the state that pass looks for.
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) continue;
// spirv-val: "Expected Image Format to match Sampled Type". A bind format
// whose class disagrees with the declaration is an application error GL
// leaves undefined; baking it would turn that into an invalid module, so it
// is declined and the image stays format-less.
if (ClassOfImageFormat(format) !=
ClassOfSampledType(irContext,
chain.imageType->GetSingleWordInOperand(kImageSampledTypeOperand))) {
continue;
}
Candidate candidate;
candidate.variable = &global;
candidate.chain = chain;
candidate.format = format;
// Validate every use BEFORE anything is mutated: a shape this pass cannot
// retype end to end has to leave the variable exactly as it found it, and a
// half-retyped module is not something a later decline could undo.
bool rewritable = true;
defUseMgr->ForEachUser(&global, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (user->opcode() == spv::Op::OpAccessChain ||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
// Only an access chain that lands ON the image - i.e. whose result is
// a pointer to the image type this pass is about to replace.
const Instruction* resultType = defUseMgr->GetDef(user->type_id());
if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypePointer ||
resultType->GetSingleWordInOperand(kPointerPointeeOperand) !=
chain.imageType->result_id()) {
rewritable = false;
return;
}
candidate.accessChains.push_back(user);
return;
}
if (user->opcode() == spv::Op::OpLoad) {
candidate.loads.push_back(user);
return;
}
// OpImageTexelPointer is DECLINED, not allowed through. Its result type
// does not depend on the format, but spirv-val requires the image behind
// an atomic to be r32i/r32ui/r32f, so baking any other format here would
// turn a module the validator accepts (Unknown is exempt) into one it
// rejects. GLSL cannot express an atomic on a format-less image anyway -
// the format qualifier is what makes an image atomic legal - so nothing
// reachable is being given up.
rewritable = false;
});
if (!rewritable) continue;
for (SizeT i = 0; i < candidate.accessChains.size() && rewritable; ++i) {
Instruction* accessChain = candidate.accessChains[i];
defUseMgr->ForEachUser(accessChain, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (user->opcode() == spv::Op::OpLoad) {
candidate.loads.push_back(user);
return;
}
rewritable = false;
});
}
if (!rewritable) continue;
for (SizeT i = 0; i < candidate.loads.size() && rewritable; ++i) {
Instruction* load = candidate.loads[i];
if (load->type_id() != chain.imageType->result_id()) {
rewritable = false;
break;
}
defUseMgr->ForEachUser(load, [&](Instruction* user) {
if (!rewritable) return;
if (IsTypeAgnosticReference(user->opcode())) return;
if (ConsumesImageValueWithoutRetyping(user->opcode())) return;
// Everything else - an image handed to a function, copied into a
// local, put in a composite - would need the type change carried
// further than this pass reasons about.
rewritable = false;
});
}
if (!rewritable) continue;
candidates.push_back(Move(candidate));
}
if (candidates.empty()) {
return Status::SuccessWithoutChange;
}
// Type cloning. A new OpTypeImage is built by hand rather than through the type
// manager because the manager always writes the OpTypeImage Access Qualifier
// operand, which is a Kernel-capability operand: emitting it into a Shader module
// is what spirv-val rejects, not what the format change needed.
//
// Each clone is inserted immediately AFTER the instruction it was cloned from.
// That is what keeps the module free of forward references: everything the
// original depended on is already defined above it, and every user of the
// original - the OpVariable among them - is below it.
std::map<std::pair<uint32_t, uint32_t>, uint32_t> cloneCache; // (original type, key) -> id
auto findIdenticalType = [&](const Instruction& candidateType) -> uint32_t {
for (const Instruction& type : irContext->types_values()) {
if (type.opcode() != candidateType.opcode()) continue;
if (type.NumInOperands() != candidateType.NumInOperands()) continue;
bool same = true;
for (uint32_t i = 0; i < type.NumInOperands(); ++i) {
if (type.GetInOperand(i).words != candidateType.GetInOperand(i).words) {
same = false;
break;
}
}
if (same) return type.result_id();
}
return 0;
};
// The later of two instructions in the globals section. Both a new type's
// ORIGINAL and the definition of the operand it was given have to precede it, and
// the second of those can be a type the module declared further down (a join, see
// below) - so the two are compared rather than assumed.
auto laterInGlobals = [&](Instruction* a, Instruction* b) -> Instruction* {
if (a == nullptr) return b;
if (b == nullptr) return a;
Instruction* last = nullptr;
for (Instruction& global : irContext->types_values()) {
if (&global == a || &global == b) last = &global;
}
return last != nullptr ? last : a;
};
// Clones `original`, replacing in-operand `operandIndex` with `value`. Returns an
// EXISTING type id when the module already declares the result: two identical
// type declarations are invalid SPIR-V, and a module that already spells the
// wanted image (say a second uniform declared `layout(r32ui)`) must be JOINED to
// that declaration, not given a second one.
//
// Placement is the other half of staying valid. SPIR-V allows no forward
// reference among types, so a clone goes after whichever of its original and its
// new operand's definition comes last - the join case is exactly where those two
// differ, and putting the clone after the original alone is what left an
// OpVariable naming a pointer type declared below it.
auto cloneTypeWithOperand = [&](Instruction* original, uint32_t operandIndex, uint32_t value,
bool valueIsId) -> uint32_t {
const auto cacheKey = std::make_pair(original->result_id(), value);
const auto cached = cloneCache.find(cacheKey);
if (cached != cloneCache.end()) return cached->second;
std::vector<Operand> operands;
operands.reserve(original->NumInOperands());
for (uint32_t i = 0; i < original->NumInOperands(); ++i) {
operands.push_back(original->GetInOperand(i));
}
auto clone = spvtools::MakeUnique<Instruction>(irContext, original->opcode(), 0,
irContext->TakeNextId(), operands);
if (clone->result_id() == 0) return 0;
clone->SetInOperand(operandIndex, {value});
const uint32_t existing = findIdenticalType(*clone);
if (existing != 0) {
cloneCache.emplace(cacheKey, existing);
return existing;
}
const uint32_t newId = clone->result_id();
// Only an ID operand names a definition the clone has to sit behind. The
// format operand is a LITERAL, and looking it up would resolve some unrelated
// instruction that happens to carry that number as its result id.
Instruction* anchor =
valueIsId ? laterInGlobals(original, defUseMgr->GetDef(value)) : original;
if (anchor == nullptr) return 0;
Instruction* inserted = clone.release();
inserted->InsertAfter(anchor);
defUseMgr->AnalyzeInstDefUse(inserted);
cloneCache.emplace(cacheKey, newId);
return newId;
};
// Types the retype leaves behind. Killed at the end when nothing references them
// any more: a stranded Unknown-format image type is legal SPIR-V but is exactly
// the thing a later reader (this pass's own probe among them) would take for a
// shader that still needs baking.
std::vector<Instruction*> possiblyOrphanedTypes;
bool changed = false;
for (Candidate& candidate : candidates) {
const uint32_t newImageId = cloneTypeWithOperand(candidate.chain.imageType, kImageFormatOperand,
static_cast<uint32_t>(candidate.format),
/*valueIsId=*/false);
if (newImageId == 0) return Status::Failure;
// The pointer-to-image type every access chain and every single-image
// variable resolves through.
uint32_t newPointeeId = newImageId;
if (candidate.chain.arrayType != nullptr) {
newPointeeId = cloneTypeWithOperand(candidate.chain.arrayType, kArrayElementOperand,
newImageId, /*valueIsId=*/true);
if (newPointeeId == 0) return Status::Failure;
}
const uint32_t newVariablePointerId = cloneTypeWithOperand(
candidate.chain.pointerType, kPointerPointeeOperand, newPointeeId, /*valueIsId=*/true);
if (newVariablePointerId == 0) return Status::Failure;
candidate.variable->SetResultType(newVariablePointerId);
defUseMgr->AnalyzeInstUse(candidate.variable);
// ...and move it behind that type, for the same no-forward-reference reason.
// A joined type can live anywhere in the globals section, including below the
// variable that now names it. Safe unconditionally because the only operand a
// UniformConstant OpVariable can have besides its storage class is an
// initializer, and a candidate carrying one was refused above.
if (Instruction* pointerTypeInst = defUseMgr->GetDef(newVariablePointerId);
pointerTypeInst != nullptr) {
candidate.variable->RemoveFromList();
candidate.variable->InsertAfter(pointerTypeInst);
}
for (Instruction* accessChain : candidate.accessChains) {
Instruction* oldResultType = defUseMgr->GetDef(accessChain->type_id());
if (oldResultType == nullptr) return Status::Failure;
const uint32_t newResultType =
cloneTypeWithOperand(oldResultType, kPointerPointeeOperand, newImageId, /*valueIsId=*/true);
if (newResultType == 0) return Status::Failure;
accessChain->SetResultType(newResultType);
defUseMgr->AnalyzeInstUse(accessChain);
possiblyOrphanedTypes.push_back(oldResultType);
}
for (Instruction* load : candidate.loads) {
load->SetResultType(newImageId);
defUseMgr->AnalyzeInstUse(load);
}
// Innermost last, so the sweep below - which only kills what nothing
// references - can free a whole chain in one walk.
possiblyOrphanedTypes.push_back(candidate.chain.pointerType);
if (candidate.chain.arrayType != nullptr) {
possiblyOrphanedTypes.push_back(candidate.chain.arrayType);
}
possiblyOrphanedTypes.push_back(candidate.chain.imageType);
// Every format outside the thirteen the Shader capability covers - the same
// thirteen GLSL ES has in core, which is not a coincidence: both lists are
// the formats Vulkan requires without an optional feature. Baking one of the
// rest without declaring the capability produces a module spirv-val rejects
// ("Operand 8 of TypeImage requires ... StorageImageExtendedFormats"), which
// is how the stencil half's r8ui announced itself.
if (!IsCoreEsslImageFormat(static_cast<Uint32>(candidate.format))) {
irContext->AddCapability(spv::Capability::StorageImageExtendedFormats);
}
changed = true;
}
if (!changed) {
return Status::SuccessWithoutChange;
}
// The types the retype stranded. Ordered outermost-first above and swept in that
// order, so a pointer goes before the image it pointed at and the image is
// unreferenced by the time it is reached. Anything still referenced - by another
// uniform this pass declined, or by an OpName - is simply left.
for (Instruction* orphan : possiblyOrphanedTypes) {
if (orphan == nullptr) continue;
if (defUseMgr->NumUsers(orphan) != 0) continue;
// Later entries may name the same instruction (several candidates sharing a
// type); scrub the duplicates before the pointer goes stale.
for (Instruction*& other : possiblyOrphanedTypes) {
if (other == orphan) other = nullptr;
}
irContext->KillInst(orphan);
}
// StorageImageWriteWithoutFormat / StorageImageReadWithoutFormat are deliberately
// left declared. A capability a module no longer exercises is valid SPIR-V, and
// dropping one is only safe after proving no format-less image is left ANYWHERE -
// including the ones this pass declined - which is a stronger claim than the
// rewrite needs to make.
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken BakeImageFormatsPass::CreateBakeImageFormatsPass(
GLFormatByName glFormatByName) {
return spvtools::Optimizer::PassToken(
spvtools::MakeUnique<BakeImageFormatsPass>(Move(glFormatByName)));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,116 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.h
// 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
#pragma once
#include "spirv-tools/optimizer.hpp"
#include "source/opt/pass.h"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Gives every format-less storage image in the module the format the application
// bound to its image unit, so SPIRV-Cross can print a format layout qualifier ESSL
// demands and desktop GLSL does not.
//
// Desktop GLSL 4.2 lets a `writeonly` (or `readonly`) image declaration omit the
// format qualifier - the access is typeless as far as the shader is concerned:
//
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
//
// GLSL ES has no such relaxation. Every image uniform must carry one, and Adreno
// says so in as many words - "all images have to define layout format" - failing the
// whole program, which is how KHR-GL4x.packed_depth_stencil.stencil_texturing's
// compute half lost its only shader.
//
// The one format that is CORRECT to print is the one glBindImageTexture named for
// that unit: GL requires the shader qualifier, the bind format and the texture's own
// internal format to belong to the same format class, so the bind format is exactly
// what the declaration would have said had it been written out. It is not knowable
// at compile time, only at draw time, which is why this is a bake into the generated
// program rather than a translation: the caller keys its build on the (unit, format)
// pairs and rebuilds when a rebind moves one (BackendProgramObjectImpl,
// MG_Backend/DirectGLES).
//
// Where the bake happens is the OpTypeImage's Image Format operand, before
// SPIRV-Cross runs, rather than in the emitted text: SPIRV-Cross prints the operand
// it is given, so setting it is the whole of the change, and the result stays a
// valid module that spirv-val can still check.
//
// Deliberately narrow, on four axes:
//
// * UNKNOWN formats only. A declared format is authoritative - a `layout(r32ui)`
// image must be read as r32ui whatever the texture behind it is - and this pass
// never overrides one. It is also what keeps the rebuild key at zero for the
// overwhelming majority of programs.
// * STORAGE images (Sampled == 2). A sampled image's format operand must stay
// Unknown; it has no format qualifier in any GLSL dialect.
// * MATCHING component class only. spirv-val requires the Image Format's component
// type to agree with the OpTypeImage's Sampled Type, so a bind format that
// disagrees with the declaration (which GL leaves undefined) is DECLINED rather
// than baked into an invalid module.
// * ESSL only. Vulkan takes an Unknown-format storage image natively given
// shaderStorageImageWriteWithoutFormat, and Magma resolves the view format from
// the same bind state at descriptor time (UniformManager), so the module must
// reach that backend unchanged.
//
// A variable whose uses are not the plain access-chain / load / image-op shape - an
// image passed to a function, stored into a local - is DECLINED individually and
// left format-less, rather than half-retyped into a module no driver would accept.
// The decision is made before anything is mutated, so a decline costs nothing.
class BakeImageFormatsPass final : public spvtools::opt::Pass {
public:
// Uniform NAME to the GL internal format bound to the image unit it addresses
// (the `format` argument of glBindImageTexture). Names are the SPIR-V ones, i.e.
// an array is named once, without a subscript. Formats with no image-format
// spelling, and names the module does not declare, are ignored.
using GLFormatByName = UnorderedMap<String, Uint>;
explicit BakeImageFormatsPass(GLFormatByName glFormatByName)
: m_glFormatByName(Move(glFormatByName)) {}
const char* name() const override { return "mobilegl-bake-image-formats"; }
Status Process() override;
// Whether the module declares a storage image with no format at all, i.e.
// whether running this pass could change anything. Answered from a single parse
// so the caller can skip the optimizer run entirely - which is every shader but
// a handful.
static bool DeclaresFormatlessStorageImage(const Vector<Uint32>& binary);
// The GL internal format's SPIR-V ImageFormat, or 0 (Unknown) when the format
// has no image-format spelling. Exposed for the caller's ESSL-side question of
// whether an extension directive is needed for it.
static Uint32 SpirvImageFormatFromGLInternalFormat(Uint glInternalFormat);
// Whether the SPIR-V ImageFormat is one GLSL ES has in core. The rest exist only
// under GL_NV_image_formats, whose directive the emitted ESSL must then carry.
// (It is also exactly the set that needs no StorageImageExtendedFormats
// capability in the module - both lists are the formats Vulkan requires without
// an optional feature.)
static bool IsCoreEsslImageFormat(Uint32 spirvImageFormat);
// Whether SPIRV-Cross will PRINT the format when it targets ESSL. Its
// is_desktop_only_format set throws instead of emitting, which loses the whole
// stage, so those formats are left for the text-level completion in the backend
// and are never baked into a module bound for SPIRV-Cross. A different question
// from IsCoreEsslImageFormat, and a different set.
static bool IsSpirvCrossEsslPrintableFormat(Uint32 spirvImageFormat);
// The ESSL layout-qualifier spelling of a GL internal format, or empty when the
// format has no image-format spelling. For the text-level completion above.
static String EsslSpellingOfGLInternalFormat(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateBakeImageFormatsPass(GLFormatByName glFormatByName);
private:
GLFormatByName m_glFormatByName;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL