From f88322ce84488ebb31fb16131066dcc9f140a299 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 11:36:52 -0400 Subject: [PATCH] [Feat, Test] (DirectGLES, ShaderTranspiler): bind atomic counter buffers end-to-end on the ES backend --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 70 ++++- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 35 +++ MobileGL/MG_Backend/DirectGLES/Managers.h | 16 ++ MobileGL/MG_IntegrationTest/CMakeLists.txt | 1 + .../Scenarios/AtomicCounterScenario.cpp | 239 ++++++++++++++++++ .../MG_Util/ShaderTranspiler/SpvcSession.cpp | 49 ++++ .../MG_Util/ShaderTranspiler/SpvcSession.h | 15 ++ MobileGL/MG_Util/ShaderTranspiler/Types.h | 2 +- 8 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7a8155f9..f33d2f80 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -395,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + void SyncAtomicCounterBuffers(const Vector& 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(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(glBinding)); + auto& obj = point.GetBoundObject(); + if (!obj) { + BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(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(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(esslBinding), + backendResource->id, static_cast(start), + static_cast(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. diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 956a3377..5d14e7e0 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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_ 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index f60a038b..6b47d8b5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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& 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& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; } + Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; } Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; } const Vector& 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 m_atomicCounterGlBindings; + Int m_atomicCounterEsslBindingTop = -1; Bool m_isInitialized = false; Bool m_backendProgramUsable = false; diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4b8b5c61..4ac76b5e 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLayeredScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp + Scenarios/AtomicCounterScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp new file mode 100644 index 00000000..55884664 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp @@ -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_ 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 +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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(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& seed) { + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer); + glBufferData(GL_ATOMIC_COUNTER_BUFFER, + static_cast(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 ReadCounters(GLuint buffer, int count) { + std::vector values(static_cast(count), 0xDEADBEEFu); + glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer); + glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, + static_cast(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 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 zeroValues = ReadCounters(zero, 2); + const std::vector 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 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 27ae0380..0cbc0cba 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -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& 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(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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h index d01434d2..f602514a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h @@ -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& 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_, 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& outGlBindings); spvc_result Compile(const char** result); const SpvcMetadata& GetMetadata() const; const char* GetLastErrorString() const; diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index 86a7e637..3eda5f31 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -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