diff --git a/CMakeLists.txt b/CMakeLists.txt index 92e64c91..720670dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,9 @@ if (ANDROID) set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE) endif() -if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) +option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF) + +if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO) # Check if ThinLTO or LTO is suppported include(CheckIPOSupported) include(CheckCCompilerFlag) @@ -248,6 +250,7 @@ set(SOURCE_FILES MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp + MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5ae819ff..e3804368 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -11,6 +11,7 @@ #include "MG_Util/Types.h" #include "Utils.h" #include "Managers.h" +#include "MultiDraw.h" #include #include #include @@ -162,37 +163,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - enum class DrawSyncBit : Uint32 { - None = 0, - IndexBuffer = 1 << 0, - IndirectBuffer = 1 << 1, - Instancing = 1 << 2 - }; - - inline DrawSyncBit operator|(DrawSyncBit a, DrawSyncBit b) { - return static_cast(static_cast(a) | static_cast(b)); - } - - inline DrawSyncBit& operator|=(DrawSyncBit& a, DrawSyncBit b) { - a = a | b; - return a; - } - - struct DrawElementsIndirectCommand { - Uint32 count = 0; - Uint32 instanceCount = 0; - Uint32 firstIndex = 0; - Int32 baseVertex = 0; - Uint32 baseInstance = 0; - }; - - struct DrawArraysIndirectCommand { - Uint32 count = 0; - Uint32 instanceCount = 0; - Uint32 first = 0; - Uint32 baseInstance = 0; - }; - SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler() { if (!g_rawDepthFetchSamplerState) { g_rawDepthFetchSamplerState = MakeShared(0); @@ -787,6 +757,11 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glTransformFeedbackVaryings != nullptr; } + Bool IsCaptureSpanOpen() { + const auto& xfb = CurrentXfb(); + return (xfb.pending || xfb.started) && !xfb.paused; + } + void BeginTransformFeedback(GLenum primitiveMode) { if (!AreTransformFeedbacksSupported()) return; auto& xfb = CurrentXfb(); @@ -2071,7 +2046,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, const SharedPtr& currentProgram); - void PrepareForDraw(DrawSyncBit syncBit) { + void PrepareForDraw(DrawSyncFlags syncBit) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif @@ -2774,6 +2749,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + Bool CurrentProgramReadsDrawID() { + const auto program = GetCurrentBackendProgram(); + return program != nullptr && program->ReadsDrawID(); + } + static Bool SupportsNativeIndirectDraws() { const auto& version = g_GLESCapabilities.GLESVersion; const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1); @@ -3080,7 +3060,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); CheckPrimitiveRestartSupported(type); g_GLESFuncs.glDrawElements(mode, count, type, indices); @@ -3090,7 +3070,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::None; + DrawSyncFlags syncBit = DrawSyncBit::None; PrepareForDraw(syncBit); const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray(); if (currentVAO) { @@ -3106,7 +3086,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); CheckPrimitiveRestartSupported(type); g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex); @@ -3116,7 +3096,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::None; + DrawSyncFlags syncBit = DrawSyncBit::None; PrepareForDraw(syncBit); const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray(); @@ -3132,18 +3112,16 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Both glMultiDrawElements entry points are emulated - ES has neither in core - by the + // tier ladder in MultiDraw.cpp, which owns the draw preparation too (its compute tier + // has to dispatch before the draw state is established). The only difference between + // them is whether the batch carries per-sub-draw base vertices. void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - CheckPrimitiveRestartSupported(type); - - for (GLsizei i = 0; i < drawcount; ++i) { - g_GLESFuncs.glDrawElements(mode, count[i], type, indices[i]); - } + MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, nullptr); } void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, @@ -3151,20 +3129,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; - PrepareForDraw(syncBit); - CheckPrimitiveRestartSupported(type); - - // Gate on the capability flag, never on the entry-point pointer: eglGetProcAddress - // returns a non-NULL stub for glMultiDrawElementsBaseVertexEXT on drivers without the - // extension interaction (NVIDIA ES), and that stub silently drops every draw. - if (g_GLESCapabilities.SupportsMultiDrawElementsBaseVertex) { - g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, basevertex); - return; - } - for (GLsizei i = 0; i < drawcount; ++i) { - g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], basevertex[i]); - } + MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex); } void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { @@ -3183,7 +3148,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const SizeT indexSize = MG_Util::GetGLTypeSize(type); @@ -3223,7 +3188,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const SizeT indexSize = MG_Util::GetGLTypeSize(type); @@ -3282,7 +3247,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const auto* commandBytes = ResolveIndirectCommandBytes( @@ -3301,20 +3266,20 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); } void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices); } void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); @@ -3323,14 +3288,14 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); } void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); @@ -3338,13 +3303,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); } void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { - DrawSyncBit syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const SizeT indexSize = MG_Util::GetGLTypeSize(type); @@ -3368,7 +3333,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) { - DrawSyncBit syncBit = DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::Instancing; PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); @@ -3376,13 +3341,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { - DrawSyncBit syncBit = DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::Instancing; PrepareForDraw(syncBit); g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); } void DrawArraysIndirect(GLenum mode, const void* indirect) { - DrawSyncBit syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const auto* commandBytes = @@ -7251,6 +7216,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void DestroyEGLContext() { BufferImpl::OnBackendContextDestroyed(); XfbImpl::OnBackendContextDestroyed(); + MultiDrawImpl::OnBackendContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed(); FramebufferImpl::InvalidateFramebufferBindingCache(); VertexArrayImpl::InvalidateVAOBindingCache(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 2b9870e7..ee1115b0 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -182,6 +182,11 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace XfbImpl { Bool AreTransformFeedbacksSupported(); + // True while a capture span is open on the current transform feedback object + // (frontend Begin seen and not paused), whether or not the deferred driver-side + // Begin has been issued yet. Draw paths that would restructure the primitive + // stream, or that need to dispatch compute mid-draw, decline while it is set. + Bool IsCaptureSpanOpen(); void BeginTransformFeedback(GLenum primitiveMode); void EndTransformFeedback(); void PauseTransformFeedback(); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index d3a27ef2..6e75950f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -36,6 +36,53 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool InProcessTeardown(); void EnsureProcessTeardownSentinel(); + // Which optional pieces of state a draw needs synchronized before it is issued. + // Index/indirect buffer syncs and the instancing-related work are skipped for + // draws that provably cannot read them. + enum class DrawSyncBit : Uint32 { + None = 0, + IndexBuffer = 1 << 0, + IndirectBuffer = 1 << 1, + Instancing = 1 << 2 + }; + // Deliberately the shared Flags<> rather than hand-written operators for this enum: + // a namespace-local operator| here would hide MobileGL::operator|(Bit, Bit) from + // every other scoped-enum flag set used inside this namespace. + using DrawSyncFlags = Flags; + + // The GL-defined indirect command layouts, byte-identical to what the driver reads + // out of a GL_DRAW_INDIRECT_BUFFER. Also the staging layout the multi-draw emulation + // synthesizes commands into. + struct DrawElementsIndirectCommand { + Uint32 count = 0; + Uint32 instanceCount = 0; + Uint32 firstIndex = 0; + Int32 baseVertex = 0; + Uint32 baseInstance = 0; + }; + + struct DrawArraysIndirectCommand { + Uint32 count = 0; + Uint32 instanceCount = 0; + Uint32 first = 0; + Uint32 baseInstance = 0; + }; + + // Brings the whole draw-relevant frontend state onto the native ES context and binds + // the program; every GL draw entry point calls it exactly once before issuing draws. + void PrepareForDraw(DrawSyncFlags syncBits); + // GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled + // the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type. + void CheckPrimitiveRestartSupported(GLenum indexType); + // Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are + // no-ops when the program does not read the corresponding builtin. + void SetCurrentBaseInstance(Uint32 baseInstance); + void SetCurrentDrawID(Uint32 drawId); + // True when the current program actually reads gl_DrawID, i.e. when a batched + // (single driver call) multi-draw tier would have to feed it one value for the whole + // batch and would therefore be wrong. + Bool CurrentProgramReadsDrawID(); + template class StateBackendObjectRegistry { public: @@ -866,6 +913,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void SetBaseInstance(Uint32 baseInstance) const; void SetBaseInstanceWordIndex(Int32 wordIndex) const; void SetDrawID(Uint32 drawId) const; + // True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID + // actually reaches a shader read rather than being discarded. + Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; } Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; } Uint GetBackendProgramId() const { return m_backendProgramId; } // False when the last SyncToBackend could not produce a usable program (a diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp new file mode 100644 index 00000000..2603492f --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp @@ -0,0 +1,894 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "MultiDraw.h" +#include "Managers.h" +#include +#include +#include + +namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { + using MG_Config::GLESMultiDrawMode; + + namespace { + // --------------------------------------------------------------------------- + // Batch shape + // --------------------------------------------------------------------------- + + SizeT IndexTypeSize(GLenum type) { + switch (type) { + case GL_UNSIGNED_BYTE: return 1; + case GL_UNSIGNED_SHORT: return 2; + case GL_UNSIGNED_INT: return 4; + default: return 0; + } + } + + // The all-ones value of an index type, which is what GL restarts on once + // primitive restart is in play. CheckPrimitiveRestartSupported has already + // rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled + // restart always restarts here and nowhere else. + Uint32 RestartSentinelFor(GLenum type) { + switch (type) { + case GL_UNSIGNED_BYTE: return 0xFFu; + case GL_UNSIGNED_SHORT: return 0xFFFFu; + default: return 0xFFFFFFFFu; + } + } + + Bool RestartActive() { + return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + } + + // Vertices per primitive for the modes whose sub-draws may be concatenated into a + // single draw without changing the primitive stream. Zero for strip/loop/fan modes + // (concatenation would weld one sub-draw's last primitive to the next sub-draw's + // first) and for GL_PATCHES, whose primitive size is dynamic tessellation state. + Uint32 ConcatenablePrimitiveSize(GLenum mode) { + switch (mode) { + case GL_POINTS: return 1; + case GL_LINES: return 2; + case GL_TRIANGLES: return 3; + case GL_LINES_ADJACENCY: return 4; + case GL_TRIANGLES_ADJACENCY: return 6; + default: return 0; + } + } + + // Beyond this an emulated batch would ask for a scratch allocation measured in + // hundreds of megabytes (and the scratch ring never shrinks again); decline and let + // a per-sub-draw tier handle it instead of trying and failing inside the driver. + constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24; + + // The flattening dispatch is one invocation per output index. ES 3.1 only + // guarantees 65535 work groups per dimension, and exceeding it makes + // glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an + // uninitialised index buffer rather than failing visibly. Cap the tier there + // instead of querying: 4.19M indices is far past any real multi-draw batch, and + // beyond it the per-sub-draw tiers are the better answer anyway. + constexpr SizeT kComputeWorkGroupSize = 64; + constexpr SizeT kMaxComputeWorkGroups = 65535; + constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize; + + Uint BoundDrawIndirectBufferId() { + const auto& indirect = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + if (!indirect) return 0; + const auto* resource = BufferImpl::EnsureBufferResource(indirect); + return resource ? resource->id : 0; + } + + const SharedPtr& BoundIndexBuffer() { + static const SharedPtr none; + const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); + if (!vao) return none; + return vao->GetIndexBufferBindingSlot().GetBoundObject(); + } + + // The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier + // that swaps in a scratch index buffer has to put back. Restoring the exact name + // matters beyond tidiness: the VAO twin memoises that it already synced this + // index binding and will not re-issue it on the next draw. + Uint BoundIndexBufferId() { + const auto& ibo = BoundIndexBuffer(); + if (!ibo) return 0; + const auto* resource = BufferImpl::EnsureBufferResource(ibo); + return resource ? resource->id : 0; + } + + // --------------------------------------------------------------------------- + // Scratch GL objects + // + // All of them belong to the ES context and are abandoned (not deleted) when it + // dies, exactly like XfbImpl's scatter buffer: the names are the dead context's + // to reclaim, and deleting them would target whatever the successor context + // handed out for the same name. + // --------------------------------------------------------------------------- + + struct ScratchBuffer { + Uint id = 0; + SizeT capacity = 0; + SizeT cursor = 0; // ring buffers only: next free byte + }; + + ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array + ScratchBuffer g_rebasedIndices; // CPU-rebased index stream + ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors + ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream + + Uint g_computeProgram = 0; + Bool g_computeProgramFailed = false; + GLint g_uElementSize = -1; + GLint g_uDrawCount = -1; + GLint g_uTotalIndices = -1; + + // Reused staging, so a steady stream of batches allocates nothing. + Vector g_commandStaging; + Vector g_indexStaging; + Vector g_drawInfoStaging; + Vector g_zeroBaseVertices; + + // Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target + // (BufferImpl::TempBufferTarget); binding it disturbs no VAO state. + Bool EnsureScratchName(ScratchBuffer& buffer) { + if (buffer.id != 0) return true; + GLuint id = 0; + g_GLESFuncs.glGenBuffers(1, &id); + if (id == 0) return false; + buffer.id = id; + buffer.capacity = 0; + buffer.cursor = 0; + return true; + } + + // Whole-buffer upload, for the two buffers that are read from offset 0 because they + // are bound as storage blocks. Respecifies rather than sub-updates: glBufferData + // orphans the previous store, so the upload never waits on a dispatch still reading + // the old contents out of the same name. + Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) { + if (bytes == 0) return true; + if (!EnsureScratchName(buffer)) return false; + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id); + // Grow in powers of two so a batch that creeps up in size stops respecifying. + SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity; + while (capacity < bytes) capacity *= 2; + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(capacity), nullptr, + GL_STREAM_DRAW); + buffer.capacity = capacity; + buffer.cursor = 0; + if (data) { + g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast(bytes), data); + } + return true; + } + + // Ring upload, for the buffers whose consumers can address a byte offset (indirect + // commands and rewritten index streams). Respecifying per batch is what an + // orphan-every-time scheme costs, and on a desktop-class driver that allocation + // dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages + // 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means + // one respecify per wrap; every byte between two wraps is written exactly once, so + // nothing in flight is overwritten, and the wrap itself orphans. + constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal + constexpr SizeT kMinRingBytes = 1u << 16; + + Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) { + outOffset = 0; + if (bytes == 0) return true; + if (!EnsureScratchName(buffer)) return false; + BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id); + + const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1); + if (buffer.capacity < aligned) { + SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity; + while (capacity < aligned) capacity *= 2; + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(capacity), nullptr, + GL_STREAM_DRAW); + buffer.capacity = capacity; + buffer.cursor = 0; + } else if (buffer.cursor + aligned > buffer.capacity) { + g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast(buffer.capacity), + nullptr, GL_STREAM_DRAW); + buffer.cursor = 0; + } + + outOffset = buffer.cursor; + if (data) { + g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast(outOffset), + static_cast(bytes), data); + } + buffer.cursor += aligned; + return true; + } + + // --------------------------------------------------------------------------- + // Tier resolution + // --------------------------------------------------------------------------- + + // Best-first, and measured rather than assumed. MobileGlues orders its own Auto + // multiindirect -> indirect -> basevertex; on both ES drivers available here that + // is backwards, because staging a command buffer per batch costs more than the + // driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws), + // ns/op, median of three: + // + // NVIDIA ES 3.2 Mesa llvmpipe ES 3.2 + // ext n/a 19300 + // basevertex 2500 25200 + // multiindirect 5700 27600 + // drawelements 5600 28700 + // indirect 5800 31000 + // + // Ring-allocating the command staging (instead of respecifying per batch) was + // tried first and moved the indirect tiers by less than noise, so the cost is the + // indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry + // point rather than an indirect one - actually beats replaying the sub-draws. + // + // The compute tier is deliberately absent from the ladder: it rewrites the + // primitive stream rather than replaying it, and it measured slowest of all here, + // so it stays opt-in behind the env knob (the same call MobileGlues makes - its + // Auto never selects Compute either). + constexpr GLESMultiDrawMode kAutoLadder[] = { + GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect, + GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements, + }; + + Bool SupportsTier(GLESMultiDrawMode tier) { + return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier); + } + + GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto; + Bool g_tierResolved = false; + String g_tierResolution; + + void ResolveTierOnce() { + if (g_tierResolved) return; + g_tierResolved = true; + g_resolvedTier = + ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode, + &g_tierResolution); + MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str()); + } + + // Which tiers have already announced themselves, one bit per GLESMultiDrawMode. + // The resolution line above says which tier was CHOSEN; this says which one a + // batch actually went through, and the two differ whenever a batch's shape + // demotes it. Worth a line each: a multi-draw path that resolves to a tier and + // then quietly runs a different one is exactly how "the batch drew nothing" + // hides. + Uint32 g_announcedTiers = 0; + + void NoteTierExecuted(GLESMultiDrawMode tier) { + const Uint32 bit = 1u << static_cast(tier); + if (g_announcedTiers & bit) return; + g_announcedTiers |= bit; + MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier)); + } + + // The tier this particular batch can actually take. A tier is demoted here when + // the batch's own shape - not the driver - rules it out; the compute tier keeps + // its remaining feasibility checks inside its implementation, where the data it + // has to walk is already in hand. + GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) { + ResolveTierOnce(); + GLESMultiDrawMode tier = g_resolvedTier; + + // Batched tiers issue one driver entry for the whole batch, so the emulated + // gl_DrawID uniform can only hold one value across every sub-draw. A program + // that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its + // own index (the spec's value); nothing else observes the difference. + const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect || + tier == GLESMultiDrawMode::Compute; + if (batched && programReadsDrawID) { + tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex + : GLESMultiDrawMode::DrawElements; + } + + // The indirect tiers describe each sub-draw as an element offset into the + // bound element array buffer. A client-memory index array has no such buffer, + // and indirect draws are not defined without one. + if (!hasIndexBuffer && + (tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) { + tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex + : GLESMultiDrawMode::DrawElements; + } + return tier; + } + + // --------------------------------------------------------------------------- + // Index rewriting, shared by the two tiers that fold base vertices into indices + // --------------------------------------------------------------------------- + + // Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping + // the source width would be wrong, not merely tight: GL adds baseVertex to the + // index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past + // 65535 addresses a vertex the source type cannot spell. Widening also gives the + // rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase. + void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex, + Bool restartActive, Uint32 restartSentinel, Uint32* out) { + const Uint32 baseVertexBits = static_cast(baseVertex); + for (SizeT i = 0; i < sourceIndexCount; ++i) { + Uint32 value = 0; + switch (indexSize) { + case 1: value = source[i]; break; + case 2: { + Uint16 narrow = 0; + std::memcpy(&narrow, source + i * 2, sizeof(narrow)); + value = narrow; + break; + } + default: std::memcpy(&value, source + i * 4, sizeof(value)); break; + } + // Unsigned wraparound is the defined behaviour for a negative base vertex. + out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits; + } + } + + // CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the + // bound index buffer or straight from the client array. Null when the sub-draw + // would read outside the buffer. + const Uint8* ResolveSubDrawIndices(const SharedPtr& indexBuffer, + const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices, + SizeT indexCount, SizeT indexSize) { + if (!indexBuffer) { + return static_cast(indices); + } + if (!indexBufferBytes) return nullptr; + const SizeT byteOffset = reinterpret_cast(indices); + const SizeT byteEnd = byteOffset + indexCount * indexSize; + if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr; + return indexBufferBytes + byteOffset; + } + + // --------------------------------------------------------------------------- + // Tier: Ext - one glMultiDrawElementsBaseVertexEXT + // --------------------------------------------------------------------------- + + Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount, + const GLint* basevertex) { + if (!SupportsTier(GLESMultiDrawMode::Ext)) return false; + const GLint* baseVertices = basevertex; + if (!baseVertices) { + // glMultiDrawElements: every base vertex is 0, but the entry point still + // wants an array. One permanently-zero vector serves every such batch. + if (g_zeroBaseVertices.size() < static_cast(drawcount)) { + g_zeroBaseVertices.resize(static_cast(drawcount), 0); + } + baseVertices = g_zeroBaseVertices.data(); + } + g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices); + NoteTierExecuted(GLESMultiDrawMode::Ext); + return true; + } + + // --------------------------------------------------------------------------- + // Tiers: MultiIndirect / Indirect - synthesized indirect commands + // --------------------------------------------------------------------------- + + Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) { + if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false; + const SizeT indexSize = IndexTypeSize(type); + if (indexSize == 0) return false; + // Indirect commands address indices as an element offset into the bound element + // array buffer, and an indirect draw is not defined without one. + const auto& indexBuffer = BoundIndexBuffer(); + if (!indexBuffer) return false; + + g_commandStaging.resize(static_cast(drawcount)); + for (GLsizei i = 0; i < drawcount; ++i) { + const SizeT byteOffset = reinterpret_cast(indices[i]); + // firstIndex counts elements, so an offset that is not a whole number of + // them cannot be expressed as a command at all. + if (byteOffset % indexSize != 0) return false; + auto& command = g_commandStaging[static_cast(i)]; + command.count = count[i] > 0 ? static_cast(count[i]) : 0u; + command.instanceCount = 1; + command.firstIndex = static_cast(byteOffset / indexSize); + command.baseVertex = basevertex ? basevertex[i] : 0; + command.baseInstance = 0; + } + + const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand); + SizeT commandBase = 0; + if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) { + return false; + } + + // Every synthesized command carries baseInstance 0. Say so through the direct + // path, which also clears the indirect-params word index a preceding real + // indirect draw may have left pointing into its own command buffer. + SetCurrentBaseInstance(0); + + const Uint previousIndirectBinding = BoundDrawIndirectBufferId(); + BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id); + if (batched) { + g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast(commandBase), + drawcount, 0); + } else { + for (GLsizei i = 0; i < drawcount; ++i) { + if (feedDrawID) SetCurrentDrawID(static_cast(i)); + const SizeT commandOffset = commandBase + static_cast(i) * sizeof(DrawElementsIndirectCommand); + g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(commandOffset)); + } + if (feedDrawID) SetCurrentDrawID(0); + } + BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding); + NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect); + return true; + } + + // --------------------------------------------------------------------------- + // Tier: BaseVertex - the per-sub-draw replay + // --------------------------------------------------------------------------- + + Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { + if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false; + for (GLsizei i = 0; i < drawcount; ++i) { + if (count[i] <= 0) continue; + if (feedDrawID) SetCurrentDrawID(static_cast(i)); + g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], + basevertex ? basevertex[i] : 0); + } + if (feedDrawID) SetCurrentDrawID(0); + NoteTierExecuted(GLESMultiDrawMode::BaseVertex); + return true; + } + + // --------------------------------------------------------------------------- + // Tier: DrawElements - base vertices folded into a scratch index stream + // --------------------------------------------------------------------------- + + Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { + const SizeT indexSize = IndexTypeSize(type); + if (indexSize == 0) return false; + + SizeT total = 0; + for (GLsizei i = 0; i < drawcount; ++i) { + if (count[i] > 0) total += static_cast(count[i]); + } + if (total == 0) return true; + if (total > kMaxFlattenedIndices) return false; + + const auto& indexBuffer = BoundIndexBuffer(); + const Uint8* indexBufferBytes = nullptr; + SizeT indexBufferSize = 0; + if (indexBuffer) { + // The shadow is the source of truth for CPU reads, but a persistent map or + // a shader write may have moved past it since the last sync. + indexBuffer->SyncPersistentMappedRange(); + indexBuffer->SyncGpuWrites(); + indexBufferBytes = indexBuffer->MappedData(); + indexBufferSize = indexBuffer->GetSize(); + } + + const Bool restartActive = RestartActive(); + const Uint32 restartSentinel = RestartSentinelFor(type); + g_indexStaging.resize(total); + SizeT cursor = 0; + for (GLsizei i = 0; i < drawcount; ++i) { + if (count[i] <= 0) continue; + const SizeT subDrawCount = static_cast(count[i]); + const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i], + subDrawCount, indexSize); + if (!source) { + MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index " + "buffer; skipping the batch", + i); + return false; + } + RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive, + restartSentinel, g_indexStaging.data() + cursor); + cursor += subDrawCount; + } + + SizeT indexBase = 0; + if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) { + return false; + } + + const Uint previousIndexBinding = BoundIndexBufferId(); + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id); + cursor = 0; + for (GLsizei i = 0; i < drawcount; ++i) { + if (count[i] <= 0) continue; + if (feedDrawID) SetCurrentDrawID(static_cast(i)); + g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT, + reinterpret_cast(indexBase + cursor * sizeof(Uint32))); + cursor += static_cast(count[i]); + } + if (feedDrawID) SetCurrentDrawID(0); + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); + NoteTierExecuted(GLESMultiDrawMode::DrawElements); + return true; + } + + // --------------------------------------------------------------------------- + // Tier: Compute - the whole batch flattened into one rebased index stream + // --------------------------------------------------------------------------- + + // One index per invocation. The sub-draw an output slot belongs to is found by + // binary search over the inclusive prefix sums of the sub-draw counts, which is + // why the descriptors are sorted by construction. Sub-draws with a zero count + // repeat the previous prefix sum and are therefore skipped by the search. + // + // Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees + // four per compute stage, so the per-sub-draw descriptors share one buffer. + constexpr const char* kFlattenComputeSource = R"(#version 310 es +layout(local_size_x = 64) in; + +uniform uint uElementSize; +uniform uint uDrawCount; +uniform uint uTotalIndices; + +layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; }; +layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; }; +layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; }; + +uint ReadSourceIndex(uint element) { + if (uElementSize == 4u) { + return sourceWords[element]; + } + if (uElementSize == 2u) { + uint word = sourceWords[element >> 1u]; + return (word >> ((element & 1u) * 16u)) & 0xFFFFu; + } + uint word = sourceWords[element >> 2u]; + return (word >> ((element & 3u) * 8u)) & 0xFFu; +} + +void main() { + uint outIndex = gl_GlobalInvocationID.x; + if (outIndex >= uTotalIndices) { + return; + } + + uint low = 0u; + uint high = uDrawCount - 1u; + while (low < high) { + uint mid = low + (high - low) / 2u; + if (drawInfo[mid * 3u + 2u] > outIndex) { + high = mid; + } else { + low = mid + 1u; + } + } + + uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]); + // Unsigned wraparound is the defined behaviour for a negative base vertex. No + // restart sentinel handling: the tier declines outright while restart is enabled. + flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u]; +} +)"; + + struct FlattenedStream { + Uint bufferId = 0; + SizeT indexCount = 0; + }; + + Bool EnsureComputeProgram() { + if (g_computeProgram != 0) return true; + if (g_computeProgramFailed) return false; + g_computeProgramFailed = true; // cleared again only on a complete success + + const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER); + if (shader == 0) { + MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed"); + return false; + } + const char* source = kFlattenComputeSource; + g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr); + g_GLESFuncs.glCompileShader(shader); + GLint status = GL_FALSE; + g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + char log[1024] = {}; + g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log); + MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log); + g_GLESFuncs.glDeleteShader(shader); + return false; + } + + const GLuint program = g_GLESFuncs.glCreateProgram(); + if (program == 0) { + MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed"); + g_GLESFuncs.glDeleteShader(shader); + return false; + } + g_GLESFuncs.glAttachShader(program, shader); + g_GLESFuncs.glLinkProgram(program); + g_GLESFuncs.glDeleteShader(shader); + g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status); + if (status != GL_TRUE) { + char log[1024] = {}; + g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log); + MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log); + g_GLESFuncs.glDeleteProgram(program); + return false; + } + + g_computeProgram = program; + g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize"); + g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount"); + g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices"); + g_computeProgramFailed = false; + MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program); + return true; + } + + // Builds the flattened stream, or leaves `out` empty when this batch's shape rules + // the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave + // the compute program current and the first storage points unbound; the + // preparation that follows re-establishes both. + void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) { + if (!SupportsTier(GLESMultiDrawMode::Compute)) return; + const SizeT indexSize = IndexTypeSize(type); + if (indexSize == 0) return; + + // Merging sub-draws into a single draw only reproduces the original primitive + // stream for list-shaped modes: a strip, loop or fan would gain primitives + // spanning the seam between two sub-draws. + const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode); + if (primitiveSize == 0) return; + + // Primitive restart defeats the whole-multiple-of-a-primitive argument below, + // even for a list mode. A restart ends the current primitive, so a sub-draw of + // six GL_TRIANGLES indices with a restart after the third emits ONE triangle + // and drops the two leftover vertices - and once concatenated those leftovers + // find a third vertex in the next sub-draw and become a triangle that GL never + // draws. Splicing separator sentinels into the flattened stream could fix it, + // at the cost of a per-sub-draw offset the prefix-sum layout does not carry; + // declining is the honest trade for a tier that is already opt-in. + if (RestartActive()) return; + + // The shader reads the source indices as a storage buffer, so there has to be + // a real buffer to read - a client-memory index array has none. + const auto& indexBuffer = BoundIndexBuffer(); + if (!indexBuffer) return; + + // A dispatch inside an open capture span is not legal, and the span would also + // observe one merged draw rather than the batch it asked for. + if (XfbImpl::IsCaptureSpanOpen()) return; + + auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer); + if (!sourceResource || sourceResource->id == 0) return; + const SizeT sourceSize = indexBuffer->GetSize(); + // std430 addresses the source as uint[]; a tail shorter than a word is not + // reachable, so a narrow index type needs a word-multiple buffer. + if (indexSize < 4 && (sourceSize % 4) != 0) return; + + g_drawInfoStaging.resize(3 * static_cast(drawcount)); + SizeT total = 0; + for (GLsizei i = 0; i < drawcount; ++i) { + const SizeT subDrawCount = count[i] > 0 ? static_cast(count[i]) : 0; + // GL drops a trailing partial primitive per sub-draw; concatenation would + // instead splice it onto the next sub-draw's first vertices. + if (subDrawCount % primitiveSize != 0) return; + const SizeT byteOffset = reinterpret_cast(indices[i]); + if (byteOffset % indexSize != 0) return; + if (subDrawCount != 0) { + const SizeT byteEnd = byteOffset + subDrawCount * indexSize; + if (byteEnd > sourceSize || byteEnd < byteOffset) return; + } + total += subDrawCount; + if (total > kMaxComputeFlattenedIndices) return; + const SizeT slot = 3 * static_cast(i); + g_drawInfoStaging[slot] = static_cast(byteOffset / indexSize); + g_drawInfoStaging[slot + 1] = static_cast(basevertex ? basevertex[i] : 0); + g_drawInfoStaging[slot + 2] = static_cast(total); + } + if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well + + if (!EnsureComputeProgram()) return; + if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) { + return; + } + if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return; + + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id); + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id); + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id); + + g_GLESFuncs.glUseProgram(g_computeProgram); + PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram; + if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast(indexSize)); + if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast(drawcount)); + if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast(total)); + + g_GLESFuncs.glDispatchCompute( + static_cast((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1); + g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT); + + // Hand the storage points back to their GL default. PrepareForDraw re-syncs + // only the points the app has actually touched, so leaving a scratch buffer on + // an untouched point would keep it visible to the next shader that declares one. + for (Uint point = 0; point < 3; ++point) { + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0); + } + + NoteTierExecuted(GLESMultiDrawMode::Compute); + out.bufferId = g_flattenedIndices.id; + out.indexCount = total; + } + } // namespace + + // ------------------------------------------------------------------------------- + // Public surface + // ------------------------------------------------------------------------------- + + Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs, + GLESMultiDrawMode tier) { + const Bool esAtLeast31 = + caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1); + switch (tier) { + case GLESMultiDrawMode::Ext: + return caps.SupportsMultiDrawElementsBaseVertex; + case GLESMultiDrawMode::MultiIndirect: + return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr; + case GLESMultiDrawMode::Indirect: + return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr; + case GLESMultiDrawMode::BaseVertex: + return caps.SupportsDrawElementsBaseVertex; + case GLESMultiDrawMode::DrawElements: + // Plain glDrawElements over a rewritten index stream: ES 2 core, so this is + // the floor every other tier can fall back to. + return true; + case GLESMultiDrawMode::Compute: + // Three storage blocks, which is inside the four ES 3.1 guarantees per stage. + return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 && + funcs.glBindBufferBase != nullptr; + case GLESMultiDrawMode::Auto: + break; + } + return false; + } + + GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested, + String* explanation) { + const auto bestAuto = [&]() { + for (const GLESMultiDrawMode tier : kAutoLadder) { + if (IsTierSupported(caps, funcs, tier)) return tier; + } + return GLESMultiDrawMode::DrawElements; + }; + + GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements; + String line; + if (requested == GLESMultiDrawMode::Auto) { + resolved = bestAuto(); + line = String("auto -> ") + TierName(resolved); + } else if (IsTierSupported(caps, funcs, requested)) { + resolved = requested; + line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved); + } else { + resolved = bestAuto(); + line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + + " requested but unsupported by this driver -> " + TierName(resolved); + } + + if (explanation) { + String supported; + for (const GLESMultiDrawMode tier : kAutoLadder) { + if (!IsTierSupported(caps, funcs, tier)) continue; + if (!supported.empty()) supported += ", "; + supported += TierName(tier); + } + if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) { + supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)"; + } + *explanation = line + " (driver supports: " + supported + ")"; + } + return resolved; + } + + const char* TierName(GLESMultiDrawMode tier) { + switch (tier) { + case GLESMultiDrawMode::Auto: return "auto"; + case GLESMultiDrawMode::Ext: return "ext"; + case GLESMultiDrawMode::MultiIndirect: return "multiindirect"; + case GLESMultiDrawMode::Indirect: return "indirect"; + case GLESMultiDrawMode::BaseVertex: return "basevertex"; + case GLESMultiDrawMode::DrawElements: return "drawelements"; + case GLESMultiDrawMode::Compute: return "compute"; + } + return "unknown"; + } + + GLESMultiDrawMode ResolvedTier() { + ResolveTierOnce(); + return g_resolvedTier; + } + + String DescribeTierResolution() { + ResolveTierOnce(); + return g_tierResolution; + } + + void OnBackendContextDestroyed() { + g_indirectCommands = {}; + g_rebasedIndices = {}; + g_drawInfo = {}; + g_flattenedIndices = {}; + g_computeProgram = 0; + g_computeProgramFailed = false; + g_uElementSize = -1; + g_uDrawCount = -1; + g_uTotalIndices = -1; + } + + void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex) { + if (drawcount <= 0 || !count || !indices) return; + // State-independent and possibly throwing, so it runs before any GL work. + CheckPrimitiveRestartSupported(type); + + const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr; + + // The compute tier dispatches BEFORE the draw state is established: doing it + // afterwards would mean unpicking the program, SSBO and index bindings + // PrepareForDraw just made, and a dispatch inside an open transform feedback + // span is not legal at all. On success it hands back a flattened index stream. + FlattenedStream flattened; + if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) { + FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened); + } + + PrepareForDraw(DrawSyncBit::IndexBuffer); + + if (flattened.indexCount != 0) { + const Uint previousIndexBinding = BoundIndexBufferId(); + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId); + g_GLESFuncs.glDrawElements(mode, static_cast(flattened.indexCount), GL_UNSIGNED_INT, nullptr); + BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); + return; + } + + const Bool feedDrawID = CurrentProgramReadsDrawID(); + const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer); + + Bool drawn = false; + switch (tier) { + case GLESMultiDrawMode::Ext: + drawn = RunExt(mode, count, type, indices, drawcount, basevertex); + break; + case GLESMultiDrawMode::MultiIndirect: + drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID); + break; + case GLESMultiDrawMode::Indirect: + drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID); + break; + case GLESMultiDrawMode::BaseVertex: + drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID); + break; + case GLESMultiDrawMode::DrawElements: + drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID); + break; + case GLESMultiDrawMode::Compute: + // Its pre-pass ran above; reaching here means it declined this batch's shape. + break; + case GLESMultiDrawMode::Auto: + break; // resolution never yields Auto + } + + // Every tier above may decline a batch whose shape it cannot express. The two + // below are the floor: a base-vertex replay where the driver has one, and the + // rewritten index stream where it does not. Both are safe for any batch these + // entry points can receive. + if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID); + if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID); + if (!drawn) { + MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); " + "the batch was dropped", + drawcount, mode, type); + } + } +} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.h b/MobileGL/MG_Backend/DirectGLES/MultiDraw.h new file mode 100644 index 00000000..b99a4b92 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.h @@ -0,0 +1,64 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include +#include +#include "DirectGLES.h" + +// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry +// points on OpenGL ES, which has neither in core. +// +// Every strategy below is an emulation; they differ only in which driver capability +// they lean on and in how many driver entries a batch of N sub-draws costs. The design +// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus +// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had: +// +// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry +// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload +// Indirect N x glDrawElementsIndirect N + 1 upload +// BaseVertex N x glDrawElementsBaseVertex N +// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload +// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry +// rebased index stream +// +// Which one runs is resolved once per ES context from the driver's capabilities, +// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch +// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp). +namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { + // The tier this ES context resolved to, computed on first use and stable after. + MG_Config::GLESMultiDrawMode ResolvedTier(); + // "multiindirect", "compute", ... - stable identifiers, also used by the POST row. + const char* TierName(MG_Config::GLESMultiDrawMode tier); + // One line naming the resolved tier, the tiers the driver can support, and the env + // clamp if one applied. For DriverPost and the startup log. + String DescribeTierResolution(); + + // The resolution itself, as a pure function of a capability set: the backend feeds + // it the live ES context's capabilities, DriverPost feeds it the ones it probed + // standalone, and both therefore report the same tier. `explanation`, when non-null, + // receives the "requested -> resolved (driver supports: ...)" line. + MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& funcs, + MG_Config::GLESMultiDrawMode requested, String* explanation); + // Whether one tier is runnable on the given capability set, for per-row POST output. + Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs, + MG_Config::GLESMultiDrawMode tier); + + // Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call + // would. `basevertex` is null for the plain glMultiDrawElements entry point (every + // base vertex is 0). Owns the whole draw, preparation included: callers must not + // have run PrepareForDraw, because the compute tier has to dispatch before the + // draw state is established. + void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, + GLsizei drawcount, const GLint* basevertex); + + // The ES context is gone: every scratch buffer and the compute program belonged to + // it, so drop the names without deleting them (the dead context reclaims them). + void OnBackendContextDestroyed(); +} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl