mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Merge branch "feat/cts-image-format-qualifier" into dev
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user