From 0ec487c9933a20bd5b262a51d655d4ed9c5e77fd Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Fri, 7 Aug 2026 08:15:54 -0400 Subject: [PATCH] [Feat] (MG_Backend): port MobileGlues' multi-draw emulation to DirectGLES as a tier ladder ES has neither glMultiDrawElements nor glMultiDrawElementsBaseVertex, so both are emulated. DirectGLES had two ways of doing it - one glMultiDrawElementsBaseVertexEXT where the driver has the extension interaction, otherwise a per-draw loop. This adds the five MobileGlues uses (gl/multidraw.cpp), so the ladder is now: one glMultiDrawElementsBaseVertexEXT; one glMultiDrawElementsIndirectEXT over a synthesized command buffer; one glDrawElementsIndirect per command over that same buffer; the base-vertex replay; plain glDrawElements over a CPU-rewritten index stream, for drivers with no base-vertex draw at all; and a compute shader that flattens the whole batch into one rebased index buffer drawn by a single glDrawElements. They live in their own translation unit that owns the entry point outright, preparation included - the compute tier has to dispatch BEFORE PrepareForDraw, or it would have to unpick the program, storage-block and index bindings the preparation just made, and a dispatch inside an open transform-feedback span is not legal at all. The auto ladder is ext -> basevertex -> multiindirect -> indirect -> drawelements, which is NOT MobileGlues' order (it puts the indirect tiers first). Measured on mc_sodium_multidraw, ns/op, median of three: NVIDIA ES 3.2 basevertex 2500 vs multiindirect 5700 and indirect 5800; Mesa llvmpipe ext 19300, basevertex 25200, multiindirect 27600, drawelements 28700, indirect 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 a real multi-draw entry point beats replaying the sub-draws. auto therefore resolves to basevertex on this box - byte for byte the behaviour that shipped - and the new tiers are what a driver with the ext interaction, or without base vertex at all, now gets. compute is never chosen by auto (nor by MobileGlues'): it rewrites the primitive stream rather than replaying it, and it measured slowest here. Four places this deliberately does not follow MobileGlues, each a correctness bug there. A rewritten stream is emitted as GL_UNSIGNED_INT whatever came in, because GL adds baseVertex at full precision and folding it into ushort indices wraps. The restart sentinel is carried across a rebase unrebased, or an enabled primitive restart is lost. The flattening tier declines strip/loop/fan modes, any sub-draw whose count is not a whole number of primitives, and any batch at all while primitive restart is enabled - a restart ends a primitive, so leftover vertices would find a third vertex in the next sub-draw and become a triangle GL never draws. And the indirect tiers decline client-memory index arrays, which have no buffer to address. gl_DrawID gets better rather than worse: the unrolled tiers now feed each sub-draw its index (the spec's value, where the old loop left the uniform untouched), and a program that actually reads it demotes the batched tiers, which can only hold one value for the whole batch. The per-batch cost is nil for the programs that do not read it. Verified: the five DirectGLES retraces are byte-identical (md5) across all six tiers on NVIDIA and on Mesa, each tier proven to have really executed rather than silently demoted, via a per-tier announcement in the log. Unit suite 421/421. The full retrace suite's five failures all reproduce unchanged on a stashed tree, so none are new. --- CMakeLists.txt | 5 +- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 106 +-- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 5 + MobileGL/MG_Backend/DirectGLES/Managers.h | 50 + MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp | 894 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/MultiDraw.h | 64 ++ 6 files changed, 1053 insertions(+), 71 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/MultiDraw.h 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