diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 4f333d4a..ba0a75c4 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1002,6 +1002,7 @@ namespace MobileGL::MG_Backend::DirectGLES { funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect; funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount; funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect; + funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount; funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex; funcsTable.GL.DrawRangeElements = DrawRangeElements; funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index c07a7e2a..daf38279 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2905,11 +2905,40 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + void SetCurrentBaseVertex(Int32 baseVertex) { + if (const auto program = GetCurrentBackendProgram()) { + program->SetBaseVertex(baseVertex); + } + } + Bool CurrentProgramReadsDrawID() { const auto program = GetCurrentBackendProgram(); return program != nullptr && program->ReadsDrawID(); } + Bool CurrentProgramReadsBaseVertex() { + const auto program = GetCurrentBackendProgram(); + return program != nullptr && program->ReadsBaseVertex(); + } + + // The two questions above, asked from BEFORE PrepareForDraw - where neither can be + // answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw + // already synced, and a twin from before a relink still carries the previous link's + // uniform locations, so "no" there means "not known yet" at least as often as it + // means no. The multi-draw compute tier has to decide whether to flatten a batch + // before PrepareForDraw runs (its dispatch cannot come after the draw state), and + // flattening a batch that turns out to need per-sub-draw values is unrecoverable - + // so an unanswerable program counts as needing them. + Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) { + const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + const auto program = GetCurrentBackendProgram(); + if (!currentProgram || program == nullptr || + program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) { + return true; + } + return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex()); + } + static Bool SupportsNativeIndirectDraws() { const auto& version = g_GLESCapabilities.GLESVersion; const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1); @@ -2947,16 +2976,28 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->id); } } + // gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read + // from the CPU shadow, so a command whose baseVertex a compute shader wrote this + // frame is not observable here (baseInstance is, through the view above). Feeding + // the stale-but-usually-correct shadow beats leaving the uniform at the previous + // draw's value, which is what a program reading gl_BaseVertex saw before. + const Bool feedBaseVertex = CurrentProgramReadsBaseVertex(); for (GLsizei i = 0; i < drawcount; ++i) { const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; SetCurrentDrawID(static_cast(i)); if (paramsBinding >= 0 && backendProgram) { // baseInstance is the 5th word of DrawElementsIndirectCommand. backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 16) / 4)); + if (feedBaseVertex) { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseVertex(cmd.baseVertex); + } } else { DrawElementsIndirectCommand cmd{}; std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); SetCurrentBaseInstance(cmd.baseInstance); + SetCurrentBaseVertex(cmd.baseVertex); } g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(cmdByteOffset)); } @@ -2969,6 +3010,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } SetCurrentDrawID(static_cast(i)); SetCurrentBaseInstance(cmd.baseInstance); + SetCurrentBaseVertex(cmd.baseVertex); const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; g_GLESFuncs.glDrawElementsInstancedBaseVertex( mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), @@ -2977,12 +3019,18 @@ namespace MobileGL::MG_Backend::DirectGLES { } SetCurrentDrawID(0); SetCurrentBaseInstance(0); + SetCurrentBaseVertex(0); } static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset, const SharedPtr& drawIndirectBuffer, GLsizei drawcount, GLsizei stride, const char* label) { (void)label; + // DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every + // command here. Written BEFORE the draws, not merely restored after them: the previous + // draw is what leaves a stale value, and restoring afterwards would only protect the + // NEXT draw while these commands ran with the stale one. + SetCurrentBaseVertex(0); const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); if (useNative) { const auto backendProgram = GetCurrentBackendProgram(); @@ -3269,7 +3317,9 @@ namespace MobileGL::MG_Backend::DirectGLES { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); CheckPrimitiveRestartSupported(type); + SetCurrentBaseVertex(basevertex); g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex); + SetCurrentBaseVertex(0); } void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { @@ -3279,6 +3329,11 @@ namespace MobileGL::MG_Backend::DirectGLES { DrawSyncFlags syncBit = DrawSyncBit::None; PrepareForDraw(syncBit); + // This loop IS the emulation - there is no batched tier for the non-indexed form - + // so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed + // ladder and the indirect executors do. Without it every sub-draw of a + // glMultiDrawArrays read draw index 0. + const Bool feedDrawID = CurrentProgramReadsDrawID(); const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray(); for (GLsizei i = 0; i < drawcount; ++i) { // Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path. @@ -3288,8 +3343,10 @@ namespace MobileGL::MG_Backend::DirectGLES { (*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first[i], count[i]); } } + if (feedDrawID) SetCurrentDrawID(static_cast(i)); g_GLESFuncs.glDrawArrays(mode, first[i], count[i]); } + if (feedDrawID) SetCurrentDrawID(0); } // Both glMultiDrawElements entry points are emulated - ES has neither in core - by the @@ -3403,6 +3460,15 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + // Both counts are read from the CPU shadow, which a buffer with no shadow does not + // have - MappedData() is null there and the reads below would be a null dereference, + // not a wrong picture. The DirectVulkan twin declines the same way. + if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) { + MGLOG_E("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + Uint32 actualDrawCount = 0; std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); @@ -3444,11 +3510,79 @@ namespace MobileGL::MG_Backend::DirectGLES { drawcount, stride, "MultiDrawArraysIndirect"); } + // The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it: + // ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the + // bound one and the batch degenerates into an ordinary indirect multi-draw of that many + // commands. Missing from the backend table until now, which made every + // glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support + // indirect-parameter array draws") on DirectGLES while the extension was advertised. + void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, + GLsizei stride) { +#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER + DebugImpl::OpenGLScopeMarker marker(__func__); +#endif + if (maxdrawcount <= 0) { + return; + } + if (stride == 0) { + stride = sizeof(DrawArraysIndirectCommand); + } + if (stride < static_cast(sizeof(DrawArraysIndirectCommand))) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu", + stride, sizeof(DrawArraysIndirectCommand)); + return; + } + + DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; + PrepareForDraw(syncBit); + + auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); + if (!drawBuffer) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound"); + return; + } + if (!parameterBuffer) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound"); + return; + } + + drawBuffer->SyncPersistentMappedRange(); + parameterBuffer->SyncPersistentMappedRange(); + + const SizeT commandOffset = reinterpret_cast(indirect); + const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + + sizeof(DrawArraysIndirectCommand); + if (commandBytes > drawBuffer->GetSize()) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); + return; + } + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); + return; + } + + // See the indexed twin: no CPU shadow means no count to read, not a wrong one. + if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) { + MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + + Uint32 actualDrawCount = 0; + std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount)); + actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); + ExecuteArraysIndirectCommands(mode, drawBuffer->MappedData() + commandOffset, commandOffset, drawBuffer, + static_cast(actualDrawCount), stride, "MultiDrawArraysIndirectCount"); + } + void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); + SetCurrentBaseVertex(basevertex); g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); + SetCurrentBaseVertex(0); } void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { @@ -3462,7 +3596,9 @@ namespace MobileGL::MG_Backend::DirectGLES { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); + SetCurrentBaseVertex(basevertex); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); + SetCurrentBaseVertex(0); SetCurrentBaseInstance(0); } @@ -3470,7 +3606,9 @@ namespace MobileGL::MG_Backend::DirectGLES { GLsizei instancecount, GLint basevertex) { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); + SetCurrentBaseVertex(basevertex); g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); + SetCurrentBaseVertex(0); } void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index bff29e7b..81e2b144 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride); void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); + void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, + GLsizei stride); void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex); void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b9f10d60..fb01c6c4 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -155,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // (possibly GPU-written) indirect command buffer, so its declaration expands into a // std430 SSBO view of that buffer indexed by a CPU-computed word index, with the plain // mg_BaseInstance uniform as the fallback for non-indirect draws. + // + // The word index is stored ONE-BASED, so that zero - the value every GLSL uniform starts + // at - is the "not an indirect draw" sentinel. Nothing seeds this uniform before a + // program's first draw, and the non-indirect draw entry points never write it at all, so a + // zero-based index with a negative sentinel would leave every such draw reading + // mg_indirectWords[0] out of a storage buffer no one bound. That is not a silent zero on a + // real driver: it returned garbage on Adreno, and a garbage gl_BaseInstance pushed the CTS + // shader_draw_parameters geometry clean off screen. String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType) { if (shaderType != GL_VERTEX_SHADER) { return source; @@ -208,12 +216,12 @@ namespace MobileGL::MG_Backend::DirectGLES { " { highp uint mg_indirectWords[]; };\n"; if (rebaseInstanceId) { machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " (gl_InstanceID - ((" + - BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" + - BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : 0))\n"; + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" + + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : 0))\n"; } machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " ((" + - BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" + - BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : " + BASE_INSTANCE_UNIFORM_NAME + ")"; + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" + + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : " + BASE_INSTANCE_UNIFORM_NAME + ")"; source.replace(pos, declaration.size(), machinery); break; } @@ -4594,6 +4602,8 @@ namespace MobileGL::MG_Backend::DirectGLES { m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_UNIFORM_NAME); m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME); + m_baseVertexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, + BASE_VERTEX_UNIFORM_NAME); m_baseInstanceWordIndexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME); // The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind @@ -4772,14 +4782,21 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast(baseInstance)); } // A direct value disables the indirect-command-buffer read. + SetBaseInstanceWordIndex(-1); + } + + // The uniform is written one-based so that its GLSL initial value, zero, already reads + // as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms. + void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const { if (m_baseInstanceWordIndexUniformLocation >= 0) { - g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, -1); + g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, + wordIndex < 0 ? 0 : wordIndex + 1); } } - void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const { - if (m_baseInstanceWordIndexUniformLocation >= 0) { - g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, wordIndex); + void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const { + if (m_baseVertexUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex); } } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index d5440906..24750c48 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -82,14 +82,26 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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. + // Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation + // uniforms. All are no-ops when the program does not read the corresponding builtin. void SetCurrentBaseInstance(Uint32 baseInstance); void SetCurrentDrawID(Uint32 drawId); + // GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every + // command that has none - including all the DrawArrays forms - so every draw path that + // does not carry one must leave this at zero rather than inherit the last draw's value. + void SetCurrentBaseVertex(Int32 baseVertex); // 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(); + // Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw + // its own base vertex through a uniform either. + Bool CurrentProgramReadsBaseVertex(); + // Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw + // has synced the program - where "does not read it" is indistinguishable from "cannot be + // asked yet". Answers true whenever the backend twin is missing or predates the current + // link. + Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices); template class StateBackendObjectRegistry { @@ -1025,9 +1037,13 @@ namespace MobileGL::MG_Backend::DirectGLES { void SetBaseInstance(Uint32 baseInstance) const; void SetBaseInstanceWordIndex(Int32 wordIndex) const; void SetDrawID(Uint32 drawId) const; + void SetBaseVertex(Int32 baseVertex) 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; } + // Same for gl_BaseVertex: only a program that reads it pays for the per-draw + // uniform write, and only such a program needs the reset after one. + Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 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 @@ -1077,6 +1093,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint m_backendGlobalUBOId = 0; Int m_baseInstanceUniformLocation = -1; Int m_drawIdUniformLocation = -1; + Int m_baseVertexUniformLocation = -1; Int m_baseInstanceWordIndexUniformLocation = -1; Int m_indirectParamsBinding = -1; Uint32 m_snormFallbackClampOutputMask = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp index 2603492f..5756c452 100644 --- a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp @@ -274,17 +274,22 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { // 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) { + GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex, + 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. + // own index (the spec's value); nothing else observes the difference. The + // emulated gl_BaseVertex is one uniform for the same reason, so a batch whose + // sub-draws carry their own base vertices unrolls too - even the Ext tier, + // which hands the driver the whole basevertex array, can only leave ONE value + // in the uniform the shader reads. const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Compute; - if (batched && programReadsDrawID) { + if (batched && (programReadsDrawID || perSubDrawBaseVertex)) { tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex : GLESMultiDrawMode::DrawElements; } @@ -371,7 +376,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { // --------------------------------------------------------------------------- Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, - GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) { + GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID, + Bool feedBaseVertex) { if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false; const SizeT indexSize = IndexTypeSize(type); if (indexSize == 0) return false; @@ -413,10 +419,12 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { } else { for (GLsizei i = 0; i < drawcount; ++i) { if (feedDrawID) SetCurrentDrawID(static_cast(i)); + if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); const SizeT commandOffset = commandBase + static_cast(i) * sizeof(DrawElementsIndirectCommand); g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(commandOffset)); } if (feedDrawID) SetCurrentDrawID(0); + if (feedBaseVertex) SetCurrentBaseVertex(0); } BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding); NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect); @@ -428,15 +436,17 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { // --------------------------------------------------------------------------- Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, - GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { + GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) { if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false; for (GLsizei i = 0; i < drawcount; ++i) { if (count[i] <= 0) continue; if (feedDrawID) SetCurrentDrawID(static_cast(i)); + if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], basevertex ? basevertex[i] : 0); } if (feedDrawID) SetCurrentDrawID(0); + if (feedBaseVertex) SetCurrentBaseVertex(0); NoteTierExecuted(GLESMultiDrawMode::BaseVertex); return true; } @@ -446,7 +456,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { // --------------------------------------------------------------------------- Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, - GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { + GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, + Bool feedBaseVertex) { const SizeT indexSize = IndexTypeSize(type); if (indexSize == 0) return false; @@ -500,11 +511,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { for (GLsizei i = 0; i < drawcount; ++i) { if (count[i] <= 0) continue; if (feedDrawID) SetCurrentDrawID(static_cast(i)); + // The base vertex is folded into the rewritten index stream here, so the + // driver sees none - but gl_BaseVertex still has to report the value the + // application passed for this sub-draw. + if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0); g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT, reinterpret_cast(indexBase + cursor * sizeof(Uint32))); cursor += static_cast(count[i]); } if (feedDrawID) SetCurrentDrawID(0); + if (feedBaseVertex) SetCurrentBaseVertex(0); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); NoteTierExecuted(GLESMultiDrawMode::DrawElements); return true; @@ -837,8 +853,15 @@ void main() { // 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. + // A batch whose sub-draws carry their own base vertices cannot be flattened either + // when the program reads gl_BaseVertex: one draw call leaves one uniform value. + // Asked conservatively because this decision precedes PrepareForDraw - see + // CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half: + // once the batch is one draw the values are gone, whereas declining to flatten only + // costs the unrolled tier. FlattenedStream flattened; - if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) { + if (ResolvedTier() == GLESMultiDrawMode::Compute && + !CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) { FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened); } @@ -852,8 +875,11 @@ void main() { return; } + // Now that PrepareForDraw has synced the program, both questions have real answers; + // the tier choice and the per-sub-draw feeds use those, not the guess above. const Bool feedDrawID = CurrentProgramReadsDrawID(); - const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer); + const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex(); + const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer); Bool drawn = false; switch (tier) { @@ -861,16 +887,19 @@ void main() { drawn = RunExt(mode, count, type, indices, drawcount, basevertex); break; case GLESMultiDrawMode::MultiIndirect: - drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID); + drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID, + feedBaseVertex); break; case GLESMultiDrawMode::Indirect: - drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID); + drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID, + feedBaseVertex); break; case GLESMultiDrawMode::BaseVertex: - drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID); + drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex); break; case GLESMultiDrawMode::DrawElements: - drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID); + drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID, + feedBaseVertex); break; case GLESMultiDrawMode::Compute: // Its pre-pass ran above; reaching here means it declined this batch's shape. @@ -883,8 +912,13 @@ void main() { // 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) { + drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex); + } + if (!drawn) { + drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID, + feedBaseVertex); + } 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", diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 41b72bcb..ce08a706 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -346,8 +346,10 @@ void main() { EXPECT_NE(rewritten.find("int instance = mg_ZeroBasedInstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos); - EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex >= 0) ? " - "int(mg_indirectWords[uint(mg_BaseInstanceWordIndex)]) : 0))"), + // One-based word index: zero is the "not an indirect draw" sentinel because that is + // the value a GLSL uniform starts at and no draw path writes it before the first draw. + EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex > 0) ? " + "int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : 0))"), MobileGL::String::npos); EXPECT_NE(rewritten.find( "layout(std430, binding = 12) readonly buffer mg_IndirectParams { highp uint mg_indirectWords[]; };"), @@ -356,6 +358,38 @@ void main() { EXPECT_EQ(CountOccurrences(rewritten, "gl_InstanceID"), 1u); } +// The sentinel itself, on the builtin it exists for. A zero-based index with a +// negative "off" value made every NON-indirect draw of such a program read +// mg_indirectWords[0] out of a storage buffer nothing had bound - the uniform starts +// at zero and no non-indirect draw path writes it - which is where the CTS +// shader_draw_parameters cases lost their geometry on Adreno. Pinned as text because +// this contract lives in two places at once: the generated ESSL below and the +1 that +// BackendProgramObjectImpl::SetBaseInstanceWordIndex applies. +TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNotIndirect) { + const ScopedGLESCapabilitiesOverride capsGuard; + auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; + caps.IndirectDrawInstanceIdIncludesBaseInstance = false; + caps.MaxShaderStorageBufferBindings = 13; + + const MobileGL::String source = R"(#version 310 es +highp int mg_BaseInstanceLowered; +void main() { + gl_Position = vec4(float(mg_BaseInstanceLowered)); +} +)"; + + const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms( + source, GL_VERTEX_SHADER); + + EXPECT_NE(rewritten.find("#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? " + "int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"), + MobileGL::String::npos) + << rewritten; + // A zero-based form would spell either of these; neither may survive. + EXPECT_EQ(rewritten.find("mg_BaseInstanceWordIndex >= 0"), MobileGL::String::npos); + EXPECT_EQ(rewritten.find("uint(mg_BaseInstanceWordIndex)"), MobileGL::String::npos); +} + TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) { const ScopedGLESCapabilitiesOverride capsGuard; auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;