[Feat, Test] (DirectGLES, ShaderTranspiler): bind atomic counter buffers end-to-end on the ES backend

This commit is contained in:
2026-08-20 11:36:52 -04:00
parent 31b5b563d6
commit f88322ce84
8 changed files with 424 additions and 3 deletions
+68 -2
View File
@@ -395,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
for (const Int glBinding : glBindings) {
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to it
// there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
static_cast<Uint>(glBinding));
auto& obj = point.GetBoundObject();
if (!obj) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
continue;
}
const auto& range = point.GetRange();
if (range.start == 0 && range.end >= obj->GetSize()) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id, static_cast<GLintptr>(start),
static_cast<GLsizeiptr>(end - start));
}
// The whole point of a counter is that the shader INCREMENTS it, and every
// conformance case reads the result back with glMapBufferRange or
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
obj->MarkGpuWritten();
}
}
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -2915,6 +2959,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
// program declares, and no other program's blocks live there. Both the draw and
// the dispatch path reach this, which is what a compute-shader counter needs.
if (!backendProgram.GetAtomicCounterBindings().empty()) {
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
backendProgram.GetAtomicCounterEsslBindingTop());
}
{
#ifdef TRACY_ENABLE
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
@@ -5683,15 +5737,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glDispatchComputeIndirect(indirect);
}
// An atomic counter is a shader storage block by the time it reaches the ES driver (glslang
// lowers every atomic_uint onto one), so an application that asks only for the counter
// barrier is asking about memory the driver knows as storage-buffer memory. Ordering one
// does not oblige a driver to order the other, so the counter bit implies the storage bit
// here - which is what the lowering costs and the only place it can be paid.
static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) {
if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) {
barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
}
return barriers;
}
void MemoryBarrier(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrier(barriers);
g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers));
if (g_GLESCapabilities.IsAngleRenderer) {
g_GLESFuncs.glFlush();
}
}
void MemoryBarrierByRegion(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers));
}
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
@@ -45,6 +45,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
// points. Those slots are taken from the TOP of the range downwards - below the one
// mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards
// never meets them, and the slot for GL binding N is `this - N` in every stage of the
// program without any shared state. Negative when the driver has no room left at all.
static Int AtomicCounterEsslBindingTop() {
return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2;
}
static Bool IsAngleLlvmpipeRenderer() {
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
}
@@ -4831,6 +4841,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
// The top is snapshotted here so every stage of this program - and the draw path
// reading it afterwards - resolves the same slot for the same GL binding.
m_atomicCounterGlBindings.clear();
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
@@ -5156,6 +5171,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
}
// Atomic counters, same mechanism for the same reason. glslang already turned
// every atomic_uint into a member of gl_AtomicCounterBlock_<N> and let the IO
// mapper pick that block's binding, which has no relation to the GL binding point
// N the application bound its counter buffer to - and can alias an SSBO the
// application binds itself. Move each block to its reserved slot and record N, so
// the draw path knows which GL_ATOMIC_COUNTER_BUFFER points to re-issue as
// storage-buffer bindings.
spvcSession.SetAtomicCounterBlockBindings(m_atomicCounterEsslBindingTop,
m_atomicCounterGlBindings);
const char* result = nullptr;
spvcSession.Compile(&result);
@@ -5303,6 +5328,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Processed shader source length: %zu", source.length());
}
// A counter buffer declared by several stages was recorded once per stage; the draw
// path binds per GL binding point, so collapse the duplicates here rather than
// re-issuing the same glBindBufferBase two or three times every draw.
if (!m_atomicCounterGlBindings.empty()) {
std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end());
m_atomicCounterGlBindings.erase(
std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()),
m_atomicCounterGlBindings.end());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
+16
View File
@@ -386,6 +386,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache();
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
// application bound never reaches the driver.
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
// (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away).
@@ -1161,6 +1168,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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; }
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
// and the top of the reserved shader-storage range their counter blocks were
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
// program that uses no atomic counter, which is what keeps the per-draw cost of the
// counter sync at one empty-vector test.
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1227,6 +1241,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_fragColorBroadcastCount = 1;
// 0 is the signature of an empty override set, i.e. what almost every program has.
Uint64 m_shaderStorageBlockBindingSignature = 0;
Vector<Int> m_atomicCounterGlBindings;
Int m_atomicCounterEsslBindingTop = -1;
Bool m_isInitialized = false;
Bool m_backendProgramUsable = false;
@@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/AtomicCounterScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,239 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.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 - ATOMIC COUNTERS, END TO END.
//
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
// closing two open ends that used to be missing entirely -
//
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
// relation to the GL binding point N the application bound its buffer to (and could alias an
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
// range; and
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
// read, is re-issued as a shader-storage binding at that reserved slot.
//
// Neither end alone is observable: with only the first the shader increments a block nobody
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
//
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
#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 {
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
// block is laid out the way GL says": a lowering that packed the members in declaration
// order without honouring `offset` would still pass a single-counter check.
constexpr const char* kCounterComputeSource = R"(#version 430 core
layout(local_size_x = 4) in;
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
void main() {
atomicCounterIncrement(g_first);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_other);
}
)";
constexpr int kLocalSizeX = 4;
constexpr int kWorkGroups = 2;
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
// that survives is also proof that the buffer's CPU-side contents reached the driver.
constexpr unsigned int kSeedFirst = 5;
constexpr unsigned int kSeedSecond = 100;
constexpr unsigned int kSeedOther = 7;
class AtomicCounterScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
GLint buffers = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
if (counters < 3 || buffers < 2) {
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
<< "; this needs 3 and 2";
}
if (!AtomicCountersAreWired()) {
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
<< "is still resolved from the shader-storage binding points";
}
m_program = CompileComputeProgram(kCounterComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
if (m_program != 0) glDeleteProgram(m_program);
m_buffers.clear();
m_program = 0;
}
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
// frontend half (limits, reflection queries, the link-time offset rules) is
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
// `binding`.
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
GL_DYNAMIC_DRAW);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
m_buffers.push_back(buffer);
return buffer;
}
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
return values;
}
void Dispatch() {
glUseProgram(m_program);
glDispatchCompute(kWorkGroups, 1, 1);
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
}
unsigned int m_program = 0;
std::string m_buildLog;
std::vector<GLuint> m_buffers;
};
} // namespace
// The counter values a dispatch leaves behind, per binding point and per offset within one
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
// before the wiring landed every one of these read back its seed unchanged.
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
Dispatch();
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
<< " means the shader's increments never reached the buffer the application bound";
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
<< "offset was not carried through the lowering, even though offset 0 was";
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
<< "resolves to a different reserved slot and is where an off-by-one shows up";
}
// A second dispatch continues from where the first left off, and a re-seed between them is
// visible to the shader. Both halves of the buffer's traffic have to work, in both
// directions: the increments are only observable through the readback path, and the re-seed
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
MakeCounterBuffer(1, {0u});
ASSERT_EQ(FirstGLError(), 0u);
Dispatch();
Dispatch();
std::vector<unsigned int> values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
const unsigned int reseed[2] = {1000u, 2000u};
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), reseed);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
Dispatch();
values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value did not reach the shader";
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
}
} // namespace MGITest
@@ -326,6 +326,55 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
// "gl_AtomicCounterBlock_5" -> 5, -1 for anything that is not one of those blocks.
// The suffix is the GL atomic-counter binding the application declared, and after
// the relaxed lowering it is the only place that number still exists.
static Int AtomicCounterBlockBinding(const char* blockName) {
if (blockName == nullptr) return -1;
const SizeT prefixLength = std::strlen(ATOMIC_COUNTER_BLOCK_PREFIX);
const String name = blockName;
if (name.length() <= prefixLength + 1) return -1;
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
if (name[prefixLength] != '_') return -1;
Int binding = 0;
for (SizeT i = prefixLength + 1; i < name.length(); ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
binding = binding * 10 + (name[i] - '0');
if (binding > 0x0FFFFFFF) return -1;
}
return binding;
}
spvc_result SpvcSession::SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings) {
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];
// The block TYPE name: glslang gives the synthesized block an EMPTY instance
// name, so resource.name carries nothing to match on. Read before Compile(),
// which is where SPIRV-Cross renames the reserved "gl_" prefix away.
const Int glBinding = AtomicCounterBlockBinding(
spvc_compiler_get_name(compiler, resource.base_type_id));
if (glBinding < 0) continue;
const Int esslBinding = topBinding - glBinding;
if (esslBinding < 0) {
MGLOG_E_ONCE("Atomic counter binding %d needs more shader storage binding points than this "
"driver has; its counters will not be updated.",
glBinding);
continue;
}
spvc_compiler_set_decoration(compiler, resource.id, SpvDecorationBinding,
static_cast<unsigned>(esslBinding));
outGlBindings.push_back(glBinding);
}
SPVC_CHK_RETURN
}
spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
@@ -105,6 +105,21 @@ namespace MobileGL {
// 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);
// Points every synthesized atomic-counter block at a RESERVED storage-block
// binding and reports which GL atomic-counter bindings the module declares.
//
// glslang's relaxed parse rewrote each atomic_uint into a member of
// gl_AtomicCounterBlock_<N>, where N is the GL binding the application declared;
// the block itself was then auto-mapped to whatever storage-block binding was
// free, which has no relation to N and can collide with an SSBO the application
// binds itself. Slot N is taken from the TOP of the driver's range downwards
// (`topBinding - N`) so the reserved window never overlaps the low bindings
// applications use, and a block whose slot would be negative is left alone and
// NOT reported - the caller binds nothing there rather than aliasing.
//
// `outGlBindings` is appended to, so one vector can collect a whole program's
// stages; it may repeat a binding declared by several of them.
spvc_result SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings);
spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;
+1 -1
View File
@@ -29,7 +29,7 @@ namespace MobileGL {
// The binding count is what the backends can actually serve. glslang lowers every
// atomic_uint onto a storage block, so one counter BUFFER costs one of the ES
// driver's shader-storage binding points, and DirectGLES reserves this many at the
// top of that range (see AtomicCounterEsslBinding in the DirectGLES managers).
// top of that range (see AtomicCounterEsslBindingTop in the DirectGLES managers).
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_BINDINGS = 8;
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, in basic machine units. Independent of the
// counter COUNTS below - it bounds the byte offset a counter may be declared at, and