mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +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
|
// A link-version mismatch means the program was relinked: the backend
|
||||||
// shaders and every cache built by CacheResourceLocations (block
|
// shaders and every cache built by CacheResourceLocations (block
|
||||||
// indices, sampler locations, UBO upload gate) are stale.
|
// 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() ||
|
if (!twin->GetBackendProgramId() ||
|
||||||
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
||||||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
||||||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount) {
|
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
||||||
|
twin->GetShaderStorageBlockBindingSignature() !=
|
||||||
|
ComputeShaderStorageBlockBindingSignature(*currentProgram)) {
|
||||||
twin->SyncToBackend(currentProgram);
|
twin->SyncToBackend(currentProgram);
|
||||||
}
|
}
|
||||||
g_currentDrawFrontendProgram = currentProgram.get();
|
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(
|
void BackendProgramObjectImpl::SyncToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -4191,6 +4216,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_E("State program object is null, skipping backend sync.");
|
MGLOG_E("State program object is null, skipping backend sync.");
|
||||||
return;
|
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
|
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
|
||||||
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
|
// 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_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||||
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
|
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
|
// Detach all existing shaders
|
||||||
GLint attachedCount = 0;
|
GLint attachedCount = 0;
|
||||||
@@ -4326,6 +4359,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
spvcSession.SetOptions(options);
|
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;
|
const char* result = nullptr;
|
||||||
spvcSession.Compile(&result);
|
spvcSession.Compile(&result);
|
||||||
|
|
||||||
@@ -4342,6 +4384,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
source = result;
|
source = result;
|
||||||
|
|
||||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
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 = RemoveLayoutBinding(source);
|
||||||
source = ProcessOutColorLocations(source);
|
source = ProcessOutColorLocations(source);
|
||||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||||
@@ -4478,11 +4529,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CacheResourceLocations(stateProgramObject);
|
CacheResourceLocations(stateProgramObject);
|
||||||
// AFTER the link, because glShaderStorageBlockBinding needs the driver's linked
|
// NOT the mechanism that makes a rebinding work - the transpiled qualifier above is.
|
||||||
// interface. This is the only place Espryt applies a rebinding: the frontend
|
// glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes,
|
||||||
// record is authoritative and the glShaderStorageBlockBinding entry point itself
|
// so this replay is a no-op almost everywhere; it stays because it is still correct
|
||||||
// deliberately never forces a program build (see DirectGLES.cpp), so a rebinding
|
// (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL
|
||||||
// requested while no backend program existed yet arrives here instead.
|
// driver used as the ES backend. AFTER the link either way, because it needs the
|
||||||
|
// driver's linked interface.
|
||||||
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
|
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
|
||||||
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
|
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
|
||||||
|
|
||||||
@@ -4490,6 +4542,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
|
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
|
// Resolves every name-based resource lookup once per link so the per-draw path
|
||||||
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
|
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
|
||||||
// glGetUniformLocation string queries; block-to-binding-point assignments are
|
// glGetUniformLocation string queries; block-to-binding-point assignments are
|
||||||
@@ -4552,7 +4622,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// is an INVALID_OPERATION.
|
// is an INVALID_OPERATION.
|
||||||
continue;
|
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;
|
if (backendLoc < 0) continue;
|
||||||
SamplerUniformBinding binding;
|
SamplerUniformBinding binding;
|
||||||
binding.frontendLocation = loc;
|
binding.frontendLocation = loc;
|
||||||
@@ -4561,8 +4641,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
binding.lastAssignedUnit = -1;
|
binding.lastAssignedUnit = -1;
|
||||||
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
|
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
|
||||||
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
|
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
|
||||||
binding.lodBiasLocation =
|
binding.lodBiasLocation = g_GLESFuncs.glGetUniformLocation(
|
||||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
|
m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + elementName).c_str());
|
||||||
binding.lastAssignedLodBias = 0.0f;
|
binding.lastAssignedLodBias = 0.0f;
|
||||||
m_samplerUniformBindings.push_back(binding);
|
m_samplerUniformBindings.push_back(binding);
|
||||||
}
|
}
|
||||||
@@ -4581,6 +4661,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_lastUsedBackendProgramId == programToBind) {
|
if (g_lastUsedBackendProgramId == programToBind) {
|
||||||
return;
|
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);
|
MGLOG_D("Using program %u", programToBind);
|
||||||
g_GLESFuncs.glUseProgram(programToBind);
|
g_GLESFuncs.glUseProgram(programToBind);
|
||||||
g_lastUsedBackendProgramId = programToBind;
|
g_lastUsedBackendProgramId = programToBind;
|
||||||
|
|||||||
@@ -1028,6 +1028,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
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; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
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);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
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;
|
Uint m_backendGlobalUBOId = 0;
|
||||||
Int m_baseInstanceUniformLocation = -1;
|
Int m_baseInstanceUniformLocation = -1;
|
||||||
Int m_drawIdUniformLocation = -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
|
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||||
Uint m_fragColorBroadcastCount = 1;
|
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_isInitialized = false;
|
||||||
Bool m_backendProgramUsable = 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
|
// 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
|
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||||
// record either way.
|
// 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);
|
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||||
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||||
// program that was just built. The frontend record is authoritative (only the
|
// program that was just built - best effort, on the same "only where the driver has
|
||||||
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||||
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||||
// reseed-on-rebuild in BuildProgramResourceCache.
|
|
||||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
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 PrgramImpl
|
||||||
|
|
||||||
namespace SamplerImpl {
|
namespace SamplerImpl {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <MG_Util/Math/HalfFloat.h>
|
#include <MG_Util/Math/HalfFloat.h>
|
||||||
#include <MG_Util/Math/SmallFloat.h>
|
#include <MG_Util/Math/SmallFloat.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -470,6 +471,352 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return result;
|
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 {
|
namespace {
|
||||||
// How a lookup carries its level of detail, and how many arguments it takes
|
// How a lookup carries its level of detail, and how many arguments it takes
|
||||||
// before the optional bias.
|
// 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.
|
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||||
String RemoveLayoutBinding(const String& glslCode);
|
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
|
// 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.
|
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
Scenarios/ProgramPipelineScenario.cpp
|
Scenarios/ProgramPipelineScenario.cpp
|
||||||
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/Glsl420DeclarationScenario.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 - GLSL 4.20 DECLARATIONS THE FRONTEND USED TO REJECT OR COLLAPSE.
|
||||||
|
//
|
||||||
|
// GLSL 4.20 gives an array of opaque uniforms or of block instances CONSECUTIVE binding
|
||||||
|
// points: "layout(binding = 1) uniform sampler2D goku[7]" puts goku[0] on texture unit 1
|
||||||
|
// and goku[6] on unit 7, and the same rule holds for "layout(binding = 2) uniform GOKU
|
||||||
|
// {...} goku[14]" over uniform buffer binding points 2..15 (GLSL 4.20 4.4.5, GL 4.6 7.6.2).
|
||||||
|
// One qualifier, N bindings - which is exactly the part that is easy to get wrong, because
|
||||||
|
// every element shares one declaration and one reflection record.
|
||||||
|
//
|
||||||
|
// Three separate mechanisms all collapsed that array down to its first element, and the
|
||||||
|
// three cases below pin one each:
|
||||||
|
//
|
||||||
|
// * the SAMPLER array (Espryt): reflection names an array after its first element at
|
||||||
|
// every location it spans, so the backend resolved "goku[0]" once per element, got one
|
||||||
|
// backend location N times, and the per-draw pass's last glUniform1i was the only one
|
||||||
|
// that survived. goku[0] ended up holding the LAST element's unit and goku[1..N-1] kept
|
||||||
|
// unit 0 - so every element sampled whatever was bound to unit 0.
|
||||||
|
// * the uniform BLOCK array (both backends): glslang reports the declared binding for
|
||||||
|
// every expanded instance, so nothing added the element offset. glGetActiveUniformBlockiv
|
||||||
|
// answered the base binding for all of them, and since both backends feed a block from
|
||||||
|
// that same number at draw time, all instances also read one buffer.
|
||||||
|
// * 'invariant' on a non-vertex stage's INPUT: legal desktop GLSL at every version, and
|
||||||
|
// ignored where it is written, but glslang rejected it from 4.20 up - so a shader that
|
||||||
|
// compiled as "#version 400" stopped compiling as "#version 420".
|
||||||
|
//
|
||||||
|
// The fourth case is the same species as the third - a legal 4.20 shader the frontend
|
||||||
|
// refused - and lives here for that reason: atomicCounterIncrement() was rejected because
|
||||||
|
// glslang applied its atomicAdd() extension gate to the atomicAdd() its own Vulkan-relaxed
|
||||||
|
// lowering had just synthesized.
|
||||||
|
//
|
||||||
|
// Conformance cases behind these: KHR-GL42.shading_language_420pack.binding_sampler_array,
|
||||||
|
// .binding_uniform_block_array, .qualifier_order[_block]_test_id_*, and
|
||||||
|
// KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#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 kElements = 4;
|
||||||
|
|
||||||
|
// No vertex attributes: the quad comes from gl_VertexID, so nothing here depends on
|
||||||
|
// the harness's attribute pinning and the fragment stage is the only thing under test.
|
||||||
|
constexpr const char* kQuadVS = R"(#version 420 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The red channel comes back as a BITMASK of which elements read the wrong thing, so
|
||||||
|
// a failure names the element instead of just saying "not green". float(bad)/255.0
|
||||||
|
// round-trips exactly through an RGBA8 target for every mask this can produce.
|
||||||
|
constexpr const char* kSamplerArrayFS = R"(#version 420 core
|
||||||
|
layout(binding = 1) uniform sampler2D goku[4];
|
||||||
|
out vec4 o_color;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
const vec2 uv = vec2(0.5, 0.5);
|
||||||
|
int bad = 0;
|
||||||
|
if (texture(goku[0], uv) != vec4(1.0, 0.0, 0.0, 1.0)) bad |= 1;
|
||||||
|
if (texture(goku[1], uv) != vec4(0.0, 0.0, 1.0, 1.0)) bad |= 2;
|
||||||
|
if (texture(goku[2], uv) != vec4(1.0, 1.0, 0.0, 1.0)) bad |= 4;
|
||||||
|
if (texture(goku[3], uv) != vec4(0.0, 1.0, 1.0, 1.0)) bad |= 8;
|
||||||
|
o_color = vec4(float(bad) / 255.0, bad == 0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kBlockArrayFS = R"(#version 420 core
|
||||||
|
layout(std140, binding = 2) uniform GOKU
|
||||||
|
{
|
||||||
|
vec4 gohan;
|
||||||
|
} goku[4];
|
||||||
|
out vec4 o_color;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
int bad = 0;
|
||||||
|
if (goku[0].gohan != vec4(1.0, 0.0, 0.0, 1.0)) bad |= 1;
|
||||||
|
if (goku[1].gohan != vec4(0.0, 0.0, 1.0, 1.0)) bad |= 2;
|
||||||
|
if (goku[2].gohan != vec4(1.0, 1.0, 0.0, 1.0)) bad |= 4;
|
||||||
|
if (goku[3].gohan != vec4(0.0, 1.0, 1.0, 1.0)) bad |= 8;
|
||||||
|
o_color = vec4(float(bad) / 255.0, bad == 0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The producing stage declares the varying invariant (always legal) and the consuming
|
||||||
|
// stage redeclares it (the part that regressed at 4.20). The qualifier ORDER is the
|
||||||
|
// shuffled one 420pack exists to allow, so this also covers the parse path the
|
||||||
|
// qualifier_order cases exercise.
|
||||||
|
constexpr const char* kInvariantInVS = R"(#version 420 core
|
||||||
|
smooth invariant out highp vec4 v_data;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
v_data = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kInvariantInFS = R"(#version 420 core
|
||||||
|
highp in smooth invariant vec4 v_data;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = v_data; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// atomicCounterIncrement() is core GLSL from 4.20 and needs no extension. MobileGL
|
||||||
|
// parses under Vulkan-relaxed rules, which rewrite it into an atomicAdd() on a buffer
|
||||||
|
// block - and glslang then applied to its OWN rewrite the desktop-below-430 gate that
|
||||||
|
// demands GL_ARB_shader_storage_buffer_object for atomicAdd, rejecting a shader it had
|
||||||
|
// just accepted. The shape is lifted from
|
||||||
|
// KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters.
|
||||||
|
constexpr const char* kAtomicCounterVS = R"(#version 420 core
|
||||||
|
layout(binding = 0, offset = 0) uniform atomic_uint g_counter;
|
||||||
|
out flat uint v_index;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
v_index = atomicCounterIncrement(g_counter);
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kAtomicCounterFS = R"(#version 420 core
|
||||||
|
in flat uint v_index;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class Glsl420DeclarationScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||||
|
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
||||||
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_textures.clear();
|
||||||
|
m_buffers.clear();
|
||||||
|
m_programs.clear();
|
||||||
|
m_vao = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint Build(const char* vs, const char* fs) {
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(vs, fs, &error);
|
||||||
|
if (program == 0) {
|
||||||
|
ADD_FAILURE() << "program did not build: " << error;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_programs.push_back(program);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One 1x1 RGBA8 texture per element, each a colour whose channels are exactly 0 or
|
||||||
|
// 255 so the shader's == comparisons are exact.
|
||||||
|
void MakeElementTextures(const std::uint8_t colors[kElements][4]) {
|
||||||
|
m_textures.assign(kElements, 0);
|
||||||
|
glGenTextures(kElements, m_textures.data());
|
||||||
|
for (int i = 0; i < kElements; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 1 + i);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_textures[i]);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, colors[i]);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||||
|
}
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MakeElementBuffers(const float values[kElements][4], GLuint firstBinding) {
|
||||||
|
m_buffers.assign(kElements, 0);
|
||||||
|
glGenBuffers(kElements, m_buffers.data());
|
||||||
|
for (int i = 0; i < kElements; ++i) {
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, m_buffers[i]);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, 4 * sizeof(float), values[i], GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, firstBinding + i, m_buffers[i]);
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the full-screen quad and hands back the centre pixel.
|
||||||
|
Rgba8 DrawAndRead(GLuint program) {
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
if (m_vao == 0) glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
const Image image = ReadPixels(gl.Width(), gl.Height());
|
||||||
|
glUseProgram(0);
|
||||||
|
return image.At(gl.Width() / 2, gl.Height() / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magma turns a sampler array into ONE descriptor with descriptorCount = N, and
|
||||||
|
// ProgramFactory::ReflectLayout refuses any descriptor array that is not a
|
||||||
|
// dynamic UBO (MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp - "descriptor
|
||||||
|
// arrays are unsupported for this descriptor kind"), so program creation fails
|
||||||
|
// and the draw samples descriptors that were never written. On a hardware driver
|
||||||
|
// that reads back as wrong pixels; under lavapipe it is a segfault in the
|
||||||
|
// rasterizer thread. Supporting it means carrying an element dimension through
|
||||||
|
// UniformManager's per-binding location tables, which is a feature, not a fix -
|
||||||
|
// so this case is SCOPED rather than disabled, because the frontend half it also
|
||||||
|
// covers (the seeded units) is real on both backends and is asserted below
|
||||||
|
// before the draw.
|
||||||
|
bool SamplerArrayDescriptorsAreSupported() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||||
|
|
||||||
|
// Same shape, different gap: with the compile fixed, this shader now links on
|
||||||
|
// both backends but paints nothing on Magma - the atomic counter becomes a
|
||||||
|
// buffer descriptor there and that half is not wired up yet (the conformance
|
||||||
|
// case KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters is where it
|
||||||
|
// is measured). The regression this case exists for is the COMPILE, which is
|
||||||
|
// asserted on both backends above; only the paint is scoped.
|
||||||
|
bool AtomicCounterDrawsAreSupported() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||||
|
|
||||||
|
static std::string BadElements(std::uint8_t mask) {
|
||||||
|
if (mask == 0) return "none";
|
||||||
|
std::string out;
|
||||||
|
for (int i = 0; i < kElements; ++i) {
|
||||||
|
if ((mask & (1u << i)) == 0) continue;
|
||||||
|
if (!out.empty()) out += ", ";
|
||||||
|
out += "[" + std::to_string(i) + "]";
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
std::vector<GLuint> m_buffers;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Element k of a sampler array samples texture unit N+k - both as the API reports it and,
|
||||||
|
// the part that was actually broken, as the draw behaves.
|
||||||
|
TEST_F(Glsl420DeclarationScenario, SamplerArrayElementsSampleConsecutiveTextureUnits) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
static const std::uint8_t colors[kElements][4] = {
|
||||||
|
{255, 0, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}, {0, 255, 255, 255}};
|
||||||
|
MakeElementTextures(colors);
|
||||||
|
|
||||||
|
const GLuint program = Build(kQuadVS, kSamplerArrayFS);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
// The reported unit is the shadow the frontend seeds from the qualifier. It was
|
||||||
|
// already right when the draw was wrong, so checking only this would have passed
|
||||||
|
// straight through the bug - it is here to separate a reflection regression from a
|
||||||
|
// backend one if this case ever fails again.
|
||||||
|
glUseProgram(program);
|
||||||
|
for (int i = 0; i < kElements; ++i) {
|
||||||
|
const std::string name = "goku[" + std::to_string(i) + "]";
|
||||||
|
const GLint location = glGetUniformLocation(program, name.c_str());
|
||||||
|
ASSERT_GE(location, 0) << name << " has no location";
|
||||||
|
GLint unit = -1;
|
||||||
|
glGetUniformiv(program, location, &unit);
|
||||||
|
EXPECT_EQ(unit, 1 + i) << name << " should default to texture unit " << (1 + i);
|
||||||
|
}
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
if (!SamplerArrayDescriptorsAreSupported()) {
|
||||||
|
GTEST_SKIP() << "sampler descriptor arrays are unimplemented on " << Gl().BackendName()
|
||||||
|
<< "; the seeded units above are the half of this case it can answer";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndRead(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(centre.r, 0) << "sampler array elements that read the wrong texture: " << BadElements(centre.r);
|
||||||
|
EXPECT_EQ(centre.g, 255) << "the draw did not reach the fragment stage at all";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance k of a uniform block array sits on buffer binding point N+k - again both as
|
||||||
|
// reported and as fed to the shader.
|
||||||
|
TEST_F(Glsl420DeclarationScenario, UniformBlockArrayInstancesTakeConsecutiveBindings) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
static const float values[kElements][4] = {
|
||||||
|
{1.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f}, {1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 1.0f, 1.0f}};
|
||||||
|
constexpr GLuint kFirstBinding = 2;
|
||||||
|
MakeElementBuffers(values, kFirstBinding);
|
||||||
|
|
||||||
|
const GLuint program = Build(kQuadVS, kBlockArrayFS);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < kElements; ++i) {
|
||||||
|
const std::string name = "GOKU[" + std::to_string(i) + "]";
|
||||||
|
const GLuint index = glGetUniformBlockIndex(program, name.c_str());
|
||||||
|
ASSERT_NE(index, static_cast<GLuint>(GL_INVALID_INDEX)) << name << " is not an active block";
|
||||||
|
GLint binding = -1;
|
||||||
|
glGetActiveUniformBlockiv(program, index, GL_UNIFORM_BLOCK_BINDING, &binding);
|
||||||
|
EXPECT_EQ(binding, static_cast<GLint>(kFirstBinding) + i)
|
||||||
|
<< name << " should start on binding point " << (kFirstBinding + i);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the block queries left a GL error behind";
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndRead(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(centre.r, 0) << "block array instances that read the wrong buffer: " << BadElements(centre.r);
|
||||||
|
EXPECT_EQ(centre.g, 255) << "the draw did not reach the fragment stage at all";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'invariant' written on a fragment input at #version 420. The same source compiles at
|
||||||
|
// #version 400 on any implementation, so a version-dependent rejection is the defect.
|
||||||
|
TEST_F(Glsl420DeclarationScenario, InvariantIsAcceptedOnANonVertexStageInput) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint program = Build(kInvariantInVS, kInvariantInFS);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndRead(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(centre.g, 255) << "the invariant-qualified varying did not arrive";
|
||||||
|
EXPECT_EQ(centre.r, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A #version 420 shader may call atomicCounterIncrement() with no extension at all. The
|
||||||
|
// assertion is deliberately the COMPILE, because the defect was a compile-time gate on
|
||||||
|
// glslang's own atomic-counter lowering; the draw that follows only checks the shader
|
||||||
|
// survives the rest of the pipeline without leaving an error behind.
|
||||||
|
TEST_F(Glsl420DeclarationScenario, AnAtomicCounterCompilesWithoutTheSsboExtension) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(shader, 1, &kAtomicCounterVS, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
FAIL() << "atomicCounterIncrement() at #version 420 core did not compile: " << log;
|
||||||
|
}
|
||||||
|
glDeleteShader(shader);
|
||||||
|
|
||||||
|
const GLuint program = Build(kAtomicCounterVS, kAtomicCounterFS);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
GLuint counter = 0;
|
||||||
|
glGenBuffers(1, &counter);
|
||||||
|
m_buffers.push_back(counter);
|
||||||
|
const GLuint zero = 0;
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, counter);
|
||||||
|
glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, counter);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
|
||||||
|
if (!AtomicCounterDrawsAreSupported()) {
|
||||||
|
GTEST_SKIP() << "atomic-counter draws do not paint on " << Gl().BackendName()
|
||||||
|
<< " yet; the compile above is what this case pins";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndRead(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -141,20 +141,6 @@ void main()
|
|||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
// glShaderStorageBlockBinding is a GL 4.3 entry point with NO equivalent in ES: a
|
|
||||||
// storage block's binding there is fixed by its layout(binding=) qualifier at link
|
|
||||||
// and cannot be changed afterwards. So Espryt, which reaches the GPU through an ES
|
|
||||||
// driver, can only honour a rebinding by baking it into the ESSL it generates -
|
|
||||||
// and it does not yet (RemoveLayoutBinding in MG_Backend/DirectGLES/Utils.cpp
|
|
||||||
// deliberately PRESERVES the declared qualifier for `buffer` declarations, which
|
|
||||||
// is what the driver then goes by). Its best-effort API replay
|
|
||||||
// (ReseedShaderStorageBlockBindings) is a no-op wherever the driver lacks the
|
|
||||||
// entry point, which is every real ES driver.
|
|
||||||
//
|
|
||||||
// Scoped rather than disabled, because the defect is per-backend and the
|
|
||||||
// pipeline-side mechanism these cases exist for is fully exercised on Magma.
|
|
||||||
bool StorageBlockRebindingIsHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
|
||||||
|
|
||||||
std::vector<GLuint> m_programs;
|
std::vector<GLuint> m_programs;
|
||||||
std::vector<GLuint> m_pipelines;
|
std::vector<GLuint> m_pipelines;
|
||||||
};
|
};
|
||||||
@@ -392,10 +378,6 @@ void main() { o_color = u_color; }
|
|||||||
if (vertexStorageBlocks < 2) {
|
if (vertexStorageBlocks < 2) {
|
||||||
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
}
|
}
|
||||||
if (!StorageBlockRebindingIsHonoured()) {
|
|
||||||
GTEST_SKIP() << "backend cannot honour glShaderStorageBlockBinding at all; see the companion "
|
|
||||||
"AStorageBlockRebindingHoldsWithoutAPipeline case";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kStorageBlockVS);
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kStorageBlockVS);
|
||||||
if (vs == 0) return;
|
if (vs == 0) return;
|
||||||
@@ -469,6 +451,12 @@ void main() { o_color = u_color; }
|
|||||||
// Two stages on purpose. Handing glUseProgram a vertex-ONLY program would confound the
|
// Two stages on purpose. Handing glUseProgram a vertex-ONLY program would confound the
|
||||||
// experiment - a program with no fragment stage is a thing some backends cannot build at
|
// experiment - a program with no fragment stage is a thing some backends cannot build at
|
||||||
// all, so its failure would say nothing about block bindings.
|
// all, so its failure would say nothing about block bindings.
|
||||||
|
//
|
||||||
|
// Runs on both backends. glShaderStorageBlockBinding is a GL 4.3 entry point with no ES
|
||||||
|
// equivalent - ES fixes a storage block's binding at link from its layout(binding=)
|
||||||
|
// qualifier - so Espryt honours a rebinding by writing the effective binding into the ESSL
|
||||||
|
// it generates (the Binding decoration is rewritten before SPIRV-Cross emits, and the draw
|
||||||
|
// path rebuilds a program whose override set has moved).
|
||||||
TEST_F(ProgramPipelineScenario, AStorageBlockRebindingHoldsWithoutAPipeline) {
|
TEST_F(ProgramPipelineScenario, AStorageBlockRebindingHoldsWithoutAPipeline) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
@@ -478,10 +466,6 @@ void main() { o_color = u_color; }
|
|||||||
if (vertexStorageBlocks < 2) {
|
if (vertexStorageBlocks < 2) {
|
||||||
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
}
|
}
|
||||||
if (!StorageBlockRebindingIsHonoured()) {
|
|
||||||
GTEST_SKIP() << "backend honours a storage-block rebinding only through the declared "
|
|
||||||
"layout(binding=) qualifier, which it does not yet rewrite";
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char* kMonolithicVS = R"(#version 430 core
|
static const char* kMonolithicVS = R"(#version 430 core
|
||||||
layout(std430) buffer Output0 { uint value0; };
|
layout(std430) buffer Output0 { uint value0; };
|
||||||
|
|||||||
@@ -41,6 +41,28 @@ namespace {
|
|||||||
return bracket == MobileGL::String::npos ? name : name.substr(0, bracket);
|
return bracket == MobileGL::String::npos ? name : name.substr(0, bracket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Element index of an arrayed interface-block instance: "GOKU[3]" -> 3, "GOKU" -> 0.
|
||||||
|
// Reflection spells arrayed instances exactly this way (glslang expands the instance
|
||||||
|
// array into one TObjectReflection per element), and the subscript it writes is a plain
|
||||||
|
// decimal, so a strict-decimal parse is both sufficient and the same rule GL 4.6
|
||||||
|
// 7.3.1.1 puts on the name a program-resource query may use.
|
||||||
|
static MobileGL::Int BlockArrayElement(const MobileGL::String& name) {
|
||||||
|
if (name.empty() || name.back() != ']') return 0;
|
||||||
|
const MobileGL::SizeT bracket = name.rfind('[');
|
||||||
|
if (bracket == MobileGL::String::npos) return 0;
|
||||||
|
const MobileGL::SizeT first = bracket + 1;
|
||||||
|
const MobileGL::SizeT last = name.length() - 1;
|
||||||
|
if (first >= last) return 0;
|
||||||
|
if (name[first] == '0' && last - first > 1) return 0; // no leading zeros
|
||||||
|
MobileGL::Int element = 0;
|
||||||
|
for (MobileGL::SizeT i = first; i < last; ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') return 0;
|
||||||
|
element = element * 10 + static_cast<MobileGL::Int>(name[i] - '0');
|
||||||
|
if (element > 0x0FFFFFFF) return 0;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
||||||
const auto* type = output.getType();
|
const auto* type = output.getType();
|
||||||
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
||||||
@@ -97,39 +119,6 @@ namespace {
|
|||||||
return std::max(1, uniform.size);
|
return std::max(1, uniform.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) {
|
|
||||||
bool inLineComment = false;
|
|
||||||
bool inBlockComment = false;
|
|
||||||
for (MobileGL::SizeT i = 0; i < source.length(); ++i) {
|
|
||||||
if (inLineComment) {
|
|
||||||
inLineComment = source[i] != '\n';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inBlockComment) {
|
|
||||||
if (source[i] == '*' && i + 1 < source.length() && source[i + 1] == '/') {
|
|
||||||
inBlockComment = false;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (source[i] == '/' && i + 1 < source.length()) {
|
|
||||||
if (source[i + 1] == '/') {
|
|
||||||
inLineComment = true;
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (source[i + 1] == '*') {
|
|
||||||
inBlockComment = true;
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (source.compare(i, 11, "local_size_") == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
@@ -370,6 +359,31 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A compute program must have a fixed local group size, and GL states that as a
|
||||||
|
// property of the PROGRAM: "at least one" of its compute shaders declares it (GL 4.6
|
||||||
|
// core 7.13 / GLSL 4.30 4.4.1.4). MobileGL used to answer that question per SHADER,
|
||||||
|
// by scanning each source for the text "local_size_" - which rejected the perfectly
|
||||||
|
// legal shape KHR-GL42.compute_shader.build-monolithic submits, three compilation
|
||||||
|
// units of which only two carry the layout and the third holds nothing but a buffer
|
||||||
|
// block and a function. It also could not see a local size that arrived through a
|
||||||
|
// macro, and it happily accepted the substring inside an unrelated identifier.
|
||||||
|
//
|
||||||
|
// glslang already merged the units' modes at link (linkValidate.cpp mergeModes, which
|
||||||
|
// also diagnoses two units declaring CONTRADICTORY sizes), so the linked
|
||||||
|
// intermediate is the thing that knows - and asking it is both correct and free.
|
||||||
|
if (const glslang::TIntermediate* cs = artifacts.program->getIntermediate(EShLangCompute);
|
||||||
|
cs != nullptr && !cs->isLocalSizeSet()) {
|
||||||
|
artifacts.linkStatus = false;
|
||||||
|
// The gate this replaced ran before LinkProgram, so a program that failed it
|
||||||
|
// published no TProgram at all. Keep that invariant: everything downstream reads
|
||||||
|
// artifacts.program as "the linked program", and a rejected link should not leave
|
||||||
|
// one behind for a query surface to find.
|
||||||
|
artifacts.program.reset();
|
||||||
|
artifacts.infoLog = "Compute shader is missing a local_size layout declaration.";
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
|
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
|
||||||
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
|
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
|
||||||
artifacts.gsInputPrimitive = GL_NONE;
|
artifacts.gsInputPrimitive = GL_NONE;
|
||||||
@@ -510,13 +524,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
in.externalIndex, i, artifacts.infoLog));
|
in.externalIndex, i, artifacts.infoLog));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (input.stage == ShaderStage::Compute &&
|
|
||||||
!ComputeShaderDeclaresLocalSize(input.source ? *input.source : String())) {
|
|
||||||
artifacts.infoLog = "Compute shader is missing a local_size layout declaration.";
|
|
||||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String reparseLog;
|
String reparseLog;
|
||||||
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
|
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
|
||||||
if (!outShaders[i]) {
|
if (!outShaders[i]) {
|
||||||
@@ -921,8 +928,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length());
|
std::max(artifacts.uniformBlockNameMaxLength, (Int)ubo.name.length());
|
||||||
artifacts.uniformBlockIndexByName[ubo.name] = i;
|
artifacts.uniformBlockIndexByName[ubo.name] = i;
|
||||||
// if there's binding defined in shader as layout(binding = ...),
|
// if there's binding defined in shader as layout(binding = ...),
|
||||||
// retrieve it here
|
// retrieve it here.
|
||||||
artifacts.uniformBlockBinding[i] = ubo.getBinding();
|
//
|
||||||
|
// An instance array takes CONSECUTIVE binding points: "layout(binding = 2)
|
||||||
|
// uniform GOKU {...} goku[14];" puts goku[0] on 2 and goku[13] on 15 (GL 4.6
|
||||||
|
// 7.6.2 / GLSL 4.20 4.4.5). glslang expands the array into one reflection
|
||||||
|
// record per element but hands every one of them the DECLARED binding, because
|
||||||
|
// they all share the block's TType - so the element offset has to be added
|
||||||
|
// here. Without it every element reported the base binding, and since both
|
||||||
|
// backends feed a block from GetUniformBlockBinding() at draw time
|
||||||
|
// (DirectGLES.cpp / UniformManager.cpp), all 14 elements also read the same
|
||||||
|
// buffer. This is the rule the storage-block path in ProgramInterface.cpp
|
||||||
|
// already applies, and whose comment there claims uniform blocks follow.
|
||||||
|
const Int declaredBinding = ubo.getBinding();
|
||||||
|
artifacts.uniformBlockBinding[i] =
|
||||||
|
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
|
||||||
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
||||||
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
EsslShaderPassTest
|
||||||
|
EsslShaderPassTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(EsslShaderPassTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
EsslShaderPassTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.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
|
||||||
|
//
|
||||||
|
// The post-transpile textual passes the DirectGLES ("Espryt") backend runs over the ESSL
|
||||||
|
// SPIRV-Cross hands it (MG_Backend/DirectGLES/Utils.cpp). No GL context and no driver: the
|
||||||
|
// passes are pure String -> String, so the shapes they have to survive can be pinned here
|
||||||
|
// instead of only on a device.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/DirectGLES/Utils.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||||
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||||
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Bool Contains(const String& haystack, const String& needle) {
|
||||||
|
return haystack.find(needle) != String::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT CountOf(const String& haystack, const String& needle) {
|
||||||
|
SizeT count = 0;
|
||||||
|
for (SizeT pos = haystack.find(needle); pos != String::npos; pos = haystack.find(needle, pos + 1)) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
||||||
|
// NonWritable+NonReadable, then clears NonReadable at the OpImageRead and NonWritable at the
|
||||||
|
// OpImageWrite, so an image the shader both reads and writes comes out carrying NEITHER
|
||||||
|
// `readonly` nor `writeonly` - which ESSL rejects for any format other than r32f/r32i/r32ui
|
||||||
|
// (GLSL ES 3.20 4.10). The device compile then fails and the draw silently binds program 0.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ReadWriteImageIsSplitIntoAnAliasingPair) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 2, rgba8) uniform highp image2D goku;
|
||||||
|
layout(location = 0) out highp vec4 mg_FragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
highp vec4 loaded = imageLoad(goku, ivec2(gl_FragCoord.xy));
|
||||||
|
imageStore(goku, ivec2(gl_FragCoord.xy), loaded + vec4(0.25));
|
||||||
|
mg_FragColor = loaded;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
|
||||||
|
// Both halves: same binding, same format, same type - which is what makes two image
|
||||||
|
// variables on one image unit legal.
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;"));
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||||
|
|
||||||
|
// The load keeps the original name, the store moves to the writeonly half.
|
||||||
|
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
||||||
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ","));
|
||||||
|
EXPECT_FALSE(Contains(out, "imageStore(goku,"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The split has to survive RemoveLayoutBinding, which runs straight after it: an ES image
|
||||||
|
// unit cannot be assigned through the API, so the layout qualifier is the only binding
|
||||||
|
// mechanism and both halves must still carry theirs afterwards.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, BothHalvesKeepTheirBindingThroughRemoveLayoutBinding) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 5, rgba8) uniform highp image2D goku;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = RemoveLayoutBinding(SplitReadWriteImageUniforms(source));
|
||||||
|
EXPECT_EQ(CountOf(out, "binding = 5"), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheap hardening: the pass does not depend on SPIRV-Cross getting the read-only case right,
|
||||||
|
// and a shader that only reads must not pay for a second uniform.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ReadOnlyImageGetsReadonlyAndIsNotSplit) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 1, rgba16f) uniform highp image2DArray trunks;
|
||||||
|
layout(location = 0) out highp vec4 mg_FragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
mg_FragColor = imageLoad(trunks, ivec3(0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba16f) uniform readonly highp image2DArray trunks;"));
|
||||||
|
EXPECT_FALSE(Contains(out, "writeonly"));
|
||||||
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||||
|
EXPECT_EQ(CountOf(out, "image2DArray"), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, WriteOnlyImageGetsWriteonlyAndIsNotSplit) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 3, rgba8) uniform highp image2D gohan;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(gohan, ivec2(0), vec4(1.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 3, rgba8) uniform writeonly highp image2D gohan;"));
|
||||||
|
EXPECT_FALSE(Contains(out, "readonly"));
|
||||||
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
// r32f / r32i / r32ui are exactly the formats GLSL ES 3.20 4.10 exempts from the rule, so a
|
||||||
|
// read+write image in one of them is already legal and must not be doubled.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ExemptFormatsAreLeftCompletelyAlone) {
|
||||||
|
for (const char* format : {"r32f", "r32i", "r32ui"}) {
|
||||||
|
const String type = String(format) == "r32f" ? "image2D" : (String(format) == "r32i" ? "iimage2D" : "uimage2D");
|
||||||
|
const String source = "#version 320 es\nlayout(binding = 4, " + String(format) + ") uniform highp " + type +
|
||||||
|
" vegeta;\nvoid main()\n{\n imageStore(vegeta, ivec2(0), imageLoad(vegeta, "
|
||||||
|
"ivec2(0)));\n}\n";
|
||||||
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source) << "format " << format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A declaration SPIRV-Cross already qualified is none of this pass's business.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreUntouched) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 0, rgba8) uniform readonly highp image2D reader;
|
||||||
|
layout(binding = 1, rgba8) uniform writeonly highp image2D writer;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The binding of an image array is the array's base; splitting must keep the array on both
|
||||||
|
// halves (dropping the subscript would silently turn 3 units into 1).
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ImageArraySplitsAndKeepsItsArraySize) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 6, rgba8) uniform highp image2D gohan[3];
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(gohan[1], ivec2(0), imageLoad(gohan[2], ivec2(0)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];"));
|
||||||
|
EXPECT_TRUE(Contains(out,
|
||||||
|
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||||
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
||||||
|
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rewrite is by identifier, not by substring: "goku" must not reach into "goku_hd", and
|
||||||
|
// the two images have to be classified independently.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ANameThatIsAPrefixOfAnotherIsNotClobbered) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 1, rgba8) uniform highp image2D goku;
|
||||||
|
layout(binding = 2, rgba8) uniform highp image2D goku_hd;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
highp vec4 loaded = imageLoad(goku, ivec2(0));
|
||||||
|
imageStore(goku, ivec2(0), loaded);
|
||||||
|
imageStore(goku_hd, ivec2(0), loaded);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
|
||||||
|
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split.
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;"));
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
||||||
|
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
||||||
|
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
||||||
|
EXPECT_FALSE(Contains(out, WriteAlias("goku_hd")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other qualifiers belong to both halves, and the memory qualifier goes where SPIRV-Cross
|
||||||
|
// puts it (right after `uniform`) so the image-rebinding regex in Managers.cpp still matches.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ExistingQualifiersAreCarriedOntoBothHalves) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 2, rgba8) uniform coherent restrict highp image2D goku;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
||||||
|
EXPECT_TRUE(
|
||||||
|
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
|
||||||
|
// such a declaration legal.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ImageSizeAloneDoesNotCountAsALoadOrAStore) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 8, rgba8ui) uniform highp uimage2D sizeOnly;
|
||||||
|
layout(location = 0) out highp vec4 mg_FragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
mg_FragColor = vec4(float(imageSize(sizeOnly).x));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "layout(binding = 8, rgba8ui) uniform readonly highp uimage2D sizeOnly;"));
|
||||||
|
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The alias must not land on an identifier the shader already uses.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, AliasNameAvoidsAnExistingIdentifier) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 6, rgba8) uniform highp image2D taken;
|
||||||
|
highp vec4 mg_imageWrite_taken;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(taken, ivec2(0), imageLoad(taken, ivec2(0)) + mg_imageWrite_taken);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_FALSE(Contains(out, "image2D " + WriteAlias("taken") + ";"));
|
||||||
|
EXPECT_TRUE(Contains(out, "image2D " + WriteAlias("taken") + "X;"));
|
||||||
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("taken") + "X,"));
|
||||||
|
EXPECT_TRUE(Contains(out, "+ mg_imageWrite_taken)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A use the pass cannot account for (here: the image handed to a user function) means it
|
||||||
|
// cannot know every store site, so it declines rather than emitting a half-rewritten shader.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, AnUnrecognizedUseLeavesTheDeclarationAlone) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 2, rgba8) uniform highp image2D passed;
|
||||||
|
highp vec4 helper(highp image2D img) { return imageLoad(img, ivec2(0)); }
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(passed, ivec2(0), helper(passed));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, ShaderWithoutImagesIsReturnedUnchanged) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 0) uniform highp sampler2D goku;
|
||||||
|
layout(location = 0) out highp vec4 mg_FragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
mg_FragColor = texture(goku, vec2(0.5));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||||
|
}
|
||||||
@@ -78,6 +78,9 @@ add_subdirectory(Query)
|
|||||||
add_subdirectory(Pipeline)
|
add_subdirectory(Pipeline)
|
||||||
add_subdirectory(ShaderTranspiler)
|
add_subdirectory(ShaderTranspiler)
|
||||||
add_subdirectory(Util)
|
add_subdirectory(Util)
|
||||||
|
# The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the
|
||||||
|
# DirectVulkan suite below this one needs no device and always builds.
|
||||||
|
add_subdirectory(Backend/DirectGLES)
|
||||||
if (ENABLE_INTEGRATION_TESTS)
|
if (ENABLE_INTEGRATION_TESTS)
|
||||||
add_subdirectory(Backend/DirectVulkan)
|
add_subdirectory(Backend/DirectVulkan)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -289,6 +289,43 @@ namespace MobileGL {
|
|||||||
SPVC_CHK_RETURN
|
SPVC_CHK_RETURN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spvc_result SpvcSession::SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings) {
|
||||||
|
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
SPVC_CHK_INIT
|
||||||
|
const spvc_reflected_resource* list = nullptr;
|
||||||
|
size_t count = 0;
|
||||||
|
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
|
||||||
|
resources, SPVC_RESOURCE_TYPE_STORAGE_BUFFER, &list, &count));
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
auto& resource = list[i];
|
||||||
|
// Two spellings, because neither one alone identifies the block the GL
|
||||||
|
// interface query named. `resource.name` is the block's instance name when
|
||||||
|
// the declaration has one; the block TYPE name (which is what the GL query
|
||||||
|
// reports for a block) lives on base_type_id. An arrayed block collapses to
|
||||||
|
// a single SPIR-V resource while GL enumerates it per element, so the bare
|
||||||
|
// name is also tried with element zero's subscript - the same convention
|
||||||
|
// ProgramObject::GetShaderStorageBlockBindingOverride documents.
|
||||||
|
const char* blockTypeName = spvc_compiler_get_name(compiler, resource.base_type_id);
|
||||||
|
const String candidates[] = {
|
||||||
|
blockTypeName != nullptr ? String(blockTypeName) : String(),
|
||||||
|
resource.name != nullptr ? String(resource.name) : String(),
|
||||||
|
};
|
||||||
|
for (const auto& candidate : candidates) {
|
||||||
|
if (candidate.empty()) continue;
|
||||||
|
auto it = bindings.find(candidate);
|
||||||
|
if (it == bindings.end()) it = bindings.find(candidate + "[0]");
|
||||||
|
if (it == bindings.end()) continue;
|
||||||
|
// Negative is "never rebound" - the declared qualifier still stands.
|
||||||
|
if (it->second < 0) break;
|
||||||
|
spvc_compiler_set_decoration(compiler, resource.id, SpvDecorationBinding,
|
||||||
|
static_cast<unsigned>(it->second));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SPVC_CHK_RETURN
|
||||||
|
}
|
||||||
|
|
||||||
spvc_result SpvcSession::Compile(const char** result) {
|
spvc_result SpvcSession::Compile(const char** result) {
|
||||||
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
||||||
SPVC_CHK_INIT
|
SPVC_CHK_INIT
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ namespace MobileGL {
|
|||||||
spvc_result SetOptions(spvc_compiler_options options);
|
spvc_result SetOptions(spvc_compiler_options options);
|
||||||
Vector<InterfaceVariable> GetShaderInterface(spvc_resource_type resource_type) const;
|
Vector<InterfaceVariable> GetShaderInterface(spvc_resource_type resource_type) const;
|
||||||
spvc_result SetVertexAttribLocation(const UnorderedMap<String, Uint>& location);
|
spvc_result SetVertexAttribLocation(const UnorderedMap<String, Uint>& location);
|
||||||
|
// Rewrites the Binding decoration of shader storage blocks before emission, so
|
||||||
|
// the generated source carries the EFFECTIVE binding rather than the declared
|
||||||
|
// one. This exists for the ESSL backend: glShaderStorageBlockBinding is a GL 4.3
|
||||||
|
// entry point with no ES equivalent (ES fixes a storage block's binding at link
|
||||||
|
// from its layout(binding=) qualifier), so the only place a rebinding can be
|
||||||
|
// expressed there is the qualifier the transpiler prints.
|
||||||
|
//
|
||||||
|
// Keyed by the GL interface-query name of the BLOCK (the block/type name; an
|
||||||
|
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
|
||||||
|
// Entries with a negative value mean "never rebound" and are skipped.
|
||||||
|
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings);
|
||||||
spvc_result Compile(const char** result);
|
spvc_result Compile(const char** result);
|
||||||
const SpvcMetadata& GetMetadata() const;
|
const SpvcMetadata& GetMetadata() const;
|
||||||
const char* GetLastErrorString() const;
|
const char* GetLastErrorString() const;
|
||||||
|
|||||||
Reference in New Issue
Block a user