mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
[Fix, Test] (MG_State, MG_Backend/DirectGLES, MG_Util, MG_IntegrationTest, MG_Test): array layout(binding=N) elements bind consecutively, a read+write image reaches ESSL legally, compute local_size comes from the linked intermediate, and glShaderStorageBlockBinding is baked into the generated source
This commit is contained in:
@@ -2077,11 +2077,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A link-version mismatch means the program was relinked: the backend
|
||||
// shaders and every cache built by CacheResourceLocations (block
|
||||
// indices, sampler locations, UBO upload gate) are stale.
|
||||
//
|
||||
// The storage-block signature is the same shape of condition: ES cannot move a
|
||||
// storage block's binding after link, so glShaderStorageBlockBinding is honoured by
|
||||
// baking the effective binding into the generated ESSL - which makes a program built
|
||||
// against a different override set stale. It is compared HERE rather than acted on in
|
||||
// the entry point because that one must never trigger a build (see
|
||||
// ShaderStorageBlockBinding below). The signature is over the values, so an
|
||||
// application that re-sets the same bindings every frame rebuilds nothing.
|
||||
if (!twin->GetBackendProgramId() ||
|
||||
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
||||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
||||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount) {
|
||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
||||
twin->GetShaderStorageBlockBindingSignature() !=
|
||||
ComputeShaderStorageBlockBindingSignature(*currentProgram)) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
|
||||
@@ -4182,6 +4182,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
const auto& overrides = stateProgramObject.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.empty()) return 0; // the overwhelming majority of programs
|
||||
// Order-independent on purpose: the source is an UnorderedMap, so any signature that
|
||||
// depended on iteration order would differ between two identical override sets and
|
||||
// rebuild the program for nothing.
|
||||
//
|
||||
// Built from the VALUES, not from a change counter, so re-setting a block to the
|
||||
// binding it already carries produces the same signature and forces no rebuild - an
|
||||
// application that calls glShaderStorageBlockBinding every frame with unchanged
|
||||
// arguments must not retranspile every frame.
|
||||
Uint64 signature = 0;
|
||||
for (const auto& [blockName, binding] : overrides) {
|
||||
if (binding < 0) continue; // never rebound; the declared qualifier still stands
|
||||
Uint64 entry = std::hash<String>{}(blockName);
|
||||
// Mixed rather than merely summed with the name hash: name and binding must not
|
||||
// be able to trade places between two entries and cancel out.
|
||||
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
|
||||
(entry << 6) + (entry >> 2));
|
||||
signature += entry; // commutative combine
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -4191,6 +4216,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("State program object is null, skipping backend sync.");
|
||||
return;
|
||||
}
|
||||
// Recorded before either early return below, so Use() can always name the GL
|
||||
// program a no-op draw belongs to - including the "linked but not drawable" exit.
|
||||
m_frontendProgramId = stateProgramObject->GetExternalIndex();
|
||||
|
||||
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
|
||||
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
|
||||
@@ -4214,6 +4242,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
|
||||
// The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the
|
||||
// transpile loop below), so the set they were generated against is part of what makes
|
||||
// this build current - the draw path compares the signature and rebuilds on a change.
|
||||
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
|
||||
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -4326,6 +4359,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
spvcSession.SetOptions(options);
|
||||
|
||||
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
|
||||
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
|
||||
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
|
||||
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
|
||||
// then deliberately preserves the qualifier for `buffer` declarations.
|
||||
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
|
||||
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
|
||||
}
|
||||
|
||||
const char* result = nullptr;
|
||||
spvcSession.Compile(&result);
|
||||
|
||||
@@ -4342,6 +4384,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
source = result;
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
// Wedged between those two on purpose:
|
||||
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
|
||||
// both halves of a split image is already the frontend texture unit (and so
|
||||
// that pass never has to reason about the alias it introduces);
|
||||
// * BEFORE RemoveLayoutBinding, whose keepBindingRegex recognises an image
|
||||
// declaration and preserves its binding - an image unit cannot be set from
|
||||
// the API in ES, so the qualifier is the only binding mechanism there is,
|
||||
// and both halves of the pair have to still be carrying theirs when it runs.
|
||||
source = SplitReadWriteImageUniforms(source);
|
||||
source = RemoveLayoutBinding(source);
|
||||
source = ProcessOutColorLocations(source);
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
@@ -4478,11 +4529,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
CacheResourceLocations(stateProgramObject);
|
||||
// AFTER the link, because glShaderStorageBlockBinding needs the driver's linked
|
||||
// interface. This is the only place Espryt applies a rebinding: the frontend
|
||||
// record is authoritative and the glShaderStorageBlockBinding entry point itself
|
||||
// deliberately never forces a program build (see DirectGLES.cpp), so a rebinding
|
||||
// requested while no backend program existed yet arrives here instead.
|
||||
// NOT the mechanism that makes a rebinding work - the transpiled qualifier above is.
|
||||
// glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes,
|
||||
// so this replay is a no-op almost everywhere; it stays because it is still correct
|
||||
// (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL
|
||||
// driver used as the ES backend. AFTER the link either way, because it needs the
|
||||
// driver's linked interface.
|
||||
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
|
||||
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
|
||||
|
||||
@@ -4490,6 +4542,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The GL name of the array element that lives at `location`, given the reflection
|
||||
// name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but
|
||||
// one location per ELEMENT, so a caller walking locations sees the same name
|
||||
// repeatedly; this turns it back into "goku[k]". Anything that is not an array
|
||||
// (or whose base location cannot be resolved) comes back unchanged, so the only
|
||||
// behaviour that moves is the array case.
|
||||
String SubscriptUniformNameForElement(const MG_State::GLState::ProgramObject& program, const String& name,
|
||||
Uint location) {
|
||||
if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name;
|
||||
const Int base = program.GetUniformLocation(name);
|
||||
if (base < 0 || static_cast<Uint>(base) > location) return name;
|
||||
const Uint element = location - static_cast<Uint>(base);
|
||||
if (element == 0) return name;
|
||||
return name.substr(0, name.size() - 3) + "[" + std::to_string(element) + "]";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Resolves every name-based resource lookup once per link so the per-draw path
|
||||
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
|
||||
// glGetUniformLocation string queries; block-to-binding-point assignments are
|
||||
@@ -4552,7 +4622,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// is an INVALID_OPERATION.
|
||||
continue;
|
||||
}
|
||||
const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, name.c_str());
|
||||
// Reflection names an array uniform after its FIRST element ("goku[0]") at
|
||||
// every location the array spans, so asking the driver for that one name
|
||||
// once per location hands back the same backend location N times. The
|
||||
// per-draw pass then issues N glUniform1i calls against it and only the
|
||||
// last element's unit survives - "layout(binding = 1) uniform sampler2D
|
||||
// goku[7]" ended up with goku[0] on unit 7 and goku[1..6] still on 0.
|
||||
// Address each element by its own name instead; the frontend already
|
||||
// reserves one location per element, so the element index is the distance
|
||||
// from the array's base location.
|
||||
const String elementName = SubscriptUniformNameForElement(*stateProgramObject, name, loc);
|
||||
const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str());
|
||||
if (backendLoc < 0) continue;
|
||||
SamplerUniformBinding binding;
|
||||
binding.frontendLocation = loc;
|
||||
@@ -4561,8 +4641,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
binding.lastAssignedUnit = -1;
|
||||
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
|
||||
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
|
||||
binding.lodBiasLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
|
||||
binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation(
|
||||
m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str());
|
||||
binding.lastAssignedLodBias = 0.0f;
|
||||
m_samplerUniformBindings.push_back(binding);
|
||||
}
|
||||
@@ -4581,6 +4661,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_lastUsedBackendProgramId == programToBind) {
|
||||
return;
|
||||
}
|
||||
if (!m_backendProgramUsable) {
|
||||
// MGLOG_I, not MGLOG_W: at MOBILEGL_LOG_LEVEL_INFO - the level the shipped
|
||||
// fordebug builds compile at - only I and F survive, and this is precisely the
|
||||
// line those builds need. Every draw made with this program renders nothing and
|
||||
// raises no GL error, so without it the only symptom is a framebuffer that kept
|
||||
// its clear colour. The early return above keeps it to at most one line per
|
||||
// program state change, not one per draw.
|
||||
MGLOG_I("Backend program for GL program %u is unusable (a shader failed to transpile, "
|
||||
"compile or link); binding program 0 - draws with it will render nothing",
|
||||
m_frontendProgramId);
|
||||
}
|
||||
MGLOG_D("Using program %u", programToBind);
|
||||
g_GLESFuncs.glUseProgram(programToBind);
|
||||
g_lastUsedBackendProgramId = programToBind;
|
||||
|
||||
@@ -1028,6 +1028,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||
// Signature of the glShaderStorageBlockBinding override set the generated ESSL was
|
||||
// transpiled against (ES can only express a storage-block binding as the declared
|
||||
// qualifier, so the overrides are baked into the source). A mismatch means the
|
||||
// program is stale exactly like the clamp masks above.
|
||||
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1048,6 +1053,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
Uint m_backendProgramId = 0;
|
||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||
// knows it by.
|
||||
Uint m_frontendProgramId = 0;
|
||||
Uint m_backendGlobalUBOId = 0;
|
||||
Int m_baseInstanceUniformLocation = -1;
|
||||
Int m_drawIdUniformLocation = -1;
|
||||
@@ -1058,6 +1067,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||
Uint m_fragColorBroadcastCount = 1;
|
||||
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
@@ -1091,14 +1102,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// on the backend program (eliminated as unused, or the driver lacks the entry
|
||||
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||
// record either way.
|
||||
//
|
||||
// NOT how a rebinding reaches the shader. glShaderStorageBlockBinding has no ES
|
||||
// equivalent and is absent from every real ES driver, so this is a no-op there;
|
||||
// SyncToBackend bakes the effective binding into the ESSL it generates instead
|
||||
// (SpvcSession::SetShaderStorageBlockBinding). This is kept as the cheaper path on
|
||||
// a driver that does happen to expose the entry point.
|
||||
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||
// program that was just built. The frontend record is authoritative (only the
|
||||
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
||||
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
||||
// reseed-on-rebuild in BuildProgramResourceCache.
|
||||
// program that was just built - best effort, on the same "only where the driver has
|
||||
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
// Order-independent digest of the program's glShaderStorageBlockBinding overrides.
|
||||
// The generated ESSL carries them (ES has no way to move a storage block's binding
|
||||
// after link), so a program built against a different set is stale and the draw path
|
||||
// has to rebuild it. Computed from the values, so re-setting a block to the binding it
|
||||
// already has costs nothing. 0 when nothing was ever rebound.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
@@ -470,6 +471,352 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsImagePassIdentifierChar(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
// Occurrences of `identifier` in `code` that are whole identifiers, i.e. not the
|
||||
// tail or head of a longer one. "goku" must not find "goku_hd" or "my_goku".
|
||||
SizeT CountIdentifierOccurrences(const String& code, const String& identifier) {
|
||||
if (identifier.empty()) return 0;
|
||||
SizeT count = 0;
|
||||
for (SizeT pos = code.find(identifier); pos != String::npos;
|
||||
pos = code.find(identifier, pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(code[pos - 1])) continue;
|
||||
const SizeT after = pos + identifier.size();
|
||||
if (after < code.size() && IsImagePassIdentifierChar(code[after])) continue;
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Bool ContainsIdentifier(const String& code, const String& identifier) {
|
||||
return CountIdentifierOccurrences(code, identifier) > 0;
|
||||
}
|
||||
|
||||
// The image format layout qualifiers ESSL accepts (GLSL ES 3.20 4.4.7 table 4.6 -
|
||||
// the ES-legal subset of what SPIRV-Cross's format_to_glsl can print). The
|
||||
// readonly/writeonly rule only applies to a declaration that carries one of them.
|
||||
Bool IsImageFormatQualifier(const String& token) {
|
||||
static constexpr StringView FORMATS[] = {
|
||||
"rgba32f", "rgba16f", "rg32f", "rg16f", "r11f_g11f_b10f",
|
||||
"r32f", "r16f", "rgba16", "rgb10_a2", "rgba8",
|
||||
"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", "rgb10_a2ui", "rgba8ui", "rg32ui", "rg16ui",
|
||||
"rg8ui", "r32ui", "r16ui", "r8ui",
|
||||
};
|
||||
for (const StringView format : FORMATS) {
|
||||
if (token == format) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// "Except for image variables qualified with the format qualifiers r32f, r32i, and
|
||||
// r32ui, image variables must specify either memory qualifier readonly or the
|
||||
// memory qualifier writeonly." (GLSL ES 3.20 4.10)
|
||||
Bool IsMemoryQualifierExemptImageFormat(const String& token) {
|
||||
return token == "r32f" || token == "r32i" || token == "r32ui";
|
||||
}
|
||||
|
||||
// Comma-separated contents of a layout(...) list, each entry trimmed.
|
||||
Vector<String> SplitLayoutQualifierList(const String& layout) {
|
||||
Vector<String> tokens;
|
||||
SizeT start = 0;
|
||||
while (start <= layout.size()) {
|
||||
SizeT comma = layout.find(',', start);
|
||||
const Bool last = comma == String::npos;
|
||||
String token = layout.substr(start, last ? String::npos : comma - start);
|
||||
const SizeT first = token.find_first_not_of(" \t\r\n");
|
||||
if (first == String::npos) {
|
||||
token.clear();
|
||||
} else {
|
||||
token = token.substr(first, token.find_last_not_of(" \t\r\n") - first + 1);
|
||||
}
|
||||
if (!token.empty()) tokens.push_back(Move(token));
|
||||
if (last) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Trims both ends and collapses every internal whitespace run to one space, so a
|
||||
// qualifier list or array suffix can be spliced back into a rebuilt declaration
|
||||
// whatever the original spacing was.
|
||||
String NormalizeDeclarationSpacing(const String& text) {
|
||||
String out;
|
||||
out.reserve(text.size());
|
||||
Bool pendingSpace = false;
|
||||
for (const char c : text) {
|
||||
if (std::isspace(static_cast<unsigned char>(c))) {
|
||||
pendingSpace = !out.empty();
|
||||
continue;
|
||||
}
|
||||
if (pendingSpace) out += ' ';
|
||||
pendingSpace = false;
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// How an image builtin touches the image it is handed.
|
||||
enum class ImageBuiltinAccess { None, Load, Store, Unknown };
|
||||
|
||||
ImageBuiltinAccess ClassifyImageBuiltin(const String& name) {
|
||||
if (name == "imageStore") return ImageBuiltinAccess::Store;
|
||||
if (name == "imageLoad") return ImageBuiltinAccess::Load;
|
||||
// imageAtomic* both reads and writes, but ES only defines the atomics on
|
||||
// r32i/r32ui/r32f images - exactly the formats the rule above exempts - so this
|
||||
// pass has already skipped any declaration they can legally appear on. Load is
|
||||
// enough to keep the classification total without ever being acted upon.
|
||||
if (name.compare(0, 11, "imageAtomic") == 0) return ImageBuiltinAccess::Load;
|
||||
if (name == "imageSize" || name == "imageSamples") return ImageBuiltinAccess::None;
|
||||
// Some other identifier that starts with "image" and is being called: not a
|
||||
// shape this pass can reason about, so it poisons the declaration instead of
|
||||
// being guessed at.
|
||||
return ImageBuiltinAccess::Unknown;
|
||||
}
|
||||
|
||||
struct ImageUniformDecl {
|
||||
String name;
|
||||
String writeName; // the writeonly half's name, when split
|
||||
String layout; // raw contents of layout(...)
|
||||
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
||||
String type; // image2D, uimage2DArray, ...
|
||||
String arraySuffix; // "" or "[7]"
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
SizeT referenceCount = 0; // uses this pass recognized and accounted for
|
||||
Bool loaded = false;
|
||||
Bool stored = false;
|
||||
Bool unknownUse = false;
|
||||
Bool split = false;
|
||||
};
|
||||
|
||||
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
||||
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
||||
// comes out of here, whichever order the two passes end up running in.
|
||||
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
||||
const String& variableName) {
|
||||
String out = "layout(" + decl.layout + ") uniform ";
|
||||
out += memoryQualifier;
|
||||
out += ' ';
|
||||
if (!decl.qualifiers.empty()) {
|
||||
out += decl.qualifiers;
|
||||
out += ' ';
|
||||
}
|
||||
out += decl.type;
|
||||
out += ' ';
|
||||
out += variableName;
|
||||
out += decl.arraySuffix;
|
||||
out += ';';
|
||||
return out;
|
||||
}
|
||||
|
||||
// A name for the writeonly half that no identifier in the shader (and no other
|
||||
// half already minted) can collide with.
|
||||
String MakeImageWriteAliasName(const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
|
||||
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
||||
// that already starts with '_' would otherwise produce.
|
||||
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
||||
doubled = candidate.find("__", doubled)) {
|
||||
candidate.erase(doubled, 1);
|
||||
}
|
||||
auto isTaken = [&](const String& identifier) {
|
||||
if (ContainsIdentifier(source, identifier)) return true;
|
||||
for (const auto& other : taken) {
|
||||
if (other == identifier) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (isTaken(candidate)) candidate += 'X';
|
||||
return candidate;
|
||||
}
|
||||
|
||||
struct ImageSourceEdit {
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
String text;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
String SplitReadWriteImageUniforms(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (glslCode.find("image") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// layout(...) uniform <memory/precision qualifiers> <image type> <name>[array];
|
||||
// The qualifier alternation is order-free even though SPIRV-Cross emits a fixed
|
||||
// order (to_qualifiers_glsl: storage, then coherent/restrict/readonly/writeonly,
|
||||
// then precision), and the array group is repeated so a hypothetical multi-
|
||||
// dimensional image array survives the round trip intact.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*((?:\[[^\]]*\]\s*)*);)");
|
||||
|
||||
Vector<ImageUniformDecl> decls;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
const String qualifiers = match[2].str();
|
||||
// Already legal: SPIRV-Cross decided one way, leave it alone.
|
||||
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bool hasFormat = false;
|
||||
Bool exemptFormat = false;
|
||||
for (const String& token : SplitLayoutQualifierList(match[1].str())) {
|
||||
if (!IsImageFormatQualifier(token)) continue;
|
||||
hasFormat = true;
|
||||
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
|
||||
}
|
||||
// No format qualifier at all is a different (and, in ES, unconditionally
|
||||
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
|
||||
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
|
||||
if (!hasFormat || exemptFormat) continue;
|
||||
|
||||
ImageUniformDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
decl.qualifiers = NormalizeDeclarationSpacing(qualifiers);
|
||||
decl.type = match[3].str();
|
||||
decl.name = match[4].str();
|
||||
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
if (decls.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
auto findDecl = [&decls](const String& name) -> SizeT {
|
||||
for (SizeT i = 0; i < decls.size(); ++i) {
|
||||
if (decls[i].name == name) return i;
|
||||
}
|
||||
return decls.size();
|
||||
};
|
||||
|
||||
// Walk every `image*(` call and attribute its first argument to a declaration.
|
||||
struct StoreSite {
|
||||
SizeT declIndex;
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
};
|
||||
Vector<StoreSite> storeSites;
|
||||
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
||||
SizeT tokenEnd = pos;
|
||||
while (tokenEnd < glslCode.size() && IsImagePassIdentifierChar(glslCode[tokenEnd])) ++tokenEnd;
|
||||
const String builtin = glslCode.substr(pos, tokenEnd - pos);
|
||||
|
||||
const SizeT openParen = glslCode.find_first_not_of(" \t\r\n", tokenEnd);
|
||||
if (openParen == String::npos || glslCode[openParen] != '(') continue; // a type, not a call
|
||||
|
||||
const SizeT argStart = glslCode.find_first_not_of(" \t\r\n", openParen + 1);
|
||||
if (argStart == String::npos) continue;
|
||||
if (!std::isalpha(static_cast<unsigned char>(glslCode[argStart])) && glslCode[argStart] != '_') {
|
||||
continue; // an expression, not a bare variable - it names no image of ours
|
||||
}
|
||||
SizeT argEnd = argStart;
|
||||
while (argEnd < glslCode.size() && IsImagePassIdentifierChar(glslCode[argEnd])) ++argEnd;
|
||||
|
||||
const SizeT declIndex = findDecl(glslCode.substr(argStart, argEnd - argStart));
|
||||
if (declIndex == decls.size()) continue;
|
||||
ImageUniformDecl& decl = decls[declIndex];
|
||||
++decl.referenceCount;
|
||||
|
||||
// The operand has to be the bare variable, optionally subscripted. Anything
|
||||
// else (a member access, a call result) is a shape this pass cannot rewrite.
|
||||
SizeT after = glslCode.find_first_not_of(" \t\r\n", argEnd);
|
||||
if (after != String::npos && glslCode[after] == '[') {
|
||||
Int depth = 0;
|
||||
SizeT scan = after;
|
||||
for (; scan < glslCode.size(); ++scan) {
|
||||
if (glslCode[scan] == '[') ++depth;
|
||||
else if (glslCode[scan] == ']' && --depth == 0) break;
|
||||
}
|
||||
after = scan >= glslCode.size() ? String::npos
|
||||
: glslCode.find_first_not_of(" \t\r\n", scan + 1);
|
||||
}
|
||||
const char nextChar = after == String::npos ? '\0' : glslCode[after];
|
||||
if (nextChar != ',' && nextChar != ')') {
|
||||
decl.unknownUse = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (ClassifyImageBuiltin(builtin)) {
|
||||
case ImageBuiltinAccess::Load:
|
||||
decl.loaded = true;
|
||||
break;
|
||||
case ImageBuiltinAccess::Store:
|
||||
decl.stored = true;
|
||||
storeSites.push_back({declIndex, argStart, argEnd - argStart});
|
||||
break;
|
||||
case ImageBuiltinAccess::None:
|
||||
break;
|
||||
default:
|
||||
decl.unknownUse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Every mention of the name has to be one this pass saw, or the split would leave
|
||||
// a store pointing at the readonly half. One occurrence is the declaration itself.
|
||||
for (auto& decl : decls) {
|
||||
if (CountIdentifierOccurrences(glslCode, decl.name) != decl.referenceCount + 1) {
|
||||
decl.unknownUse = true;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenAliases;
|
||||
for (auto& decl : decls) {
|
||||
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
||||
if (decl.loaded && decl.stored) {
|
||||
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||
takenAliases.push_back(decl.writeName);
|
||||
decl.split = true;
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
|
||||
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
|
||||
} else if (decl.stored) {
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||
} else {
|
||||
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
||||
// the qualifier that keeps every one of those legal.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name)});
|
||||
}
|
||||
}
|
||||
for (const StoreSite& site : storeSites) {
|
||||
const ImageUniformDecl& decl = decls[site.declIndex];
|
||||
if (!decl.split) continue;
|
||||
edits.push_back({site.start, site.length, decl.writeName});
|
||||
}
|
||||
if (edits.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Back to front, so an earlier edit's offsets stay valid.
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
||||
String result = glslCode;
|
||||
for (const ImageSourceEdit& edit : edits) {
|
||||
result.replace(edit.start, edit.length, edit.text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How a lookup carries its level of detail, and how many arguments it takes
|
||||
// before the optional bias.
|
||||
|
||||
@@ -131,6 +131,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||
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.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
|
||||
// speculatively decorates every storage image NonWritable+NonReadable
|
||||
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
|
||||
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
|
||||
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
|
||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||
// up as a device compile failure - and then as a silently no-op draw.
|
||||
//
|
||||
// Restores a legal declaration:
|
||||
// * loaded only -> add `readonly`
|
||||
// * stored only -> add `writeonly`
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `readonly <name>` and `writeonly
|
||||
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
|
||||
// imageStore at the second one. Several image variables
|
||||
// may share an image unit as long as they have the same
|
||||
// type and format, which is exactly what the pair is.
|
||||
//
|
||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
|
||||
// uniforms, so a shader with more than half the limit in read+write images is the case
|
||||
// to watch.
|
||||
//
|
||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||
// already rewritten to and must run before those bindings are stripped - see the call
|
||||
// site in Managers.cpp.
|
||||
String SplitReadWriteImageUniforms(const String& glslCode);
|
||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||
|
||||
Reference in New Issue
Block a user