From 1ebf191f9480804bf1635d090e4dd7aa286e4b60 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 12 Aug 2026 08:12:27 -0400 Subject: [PATCH] [Fix, Test] (MG_Backend/DirectGLES, MG_Util): baseInstance reached the shader but never the vertex fetch --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 36 ++++- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 58 ++++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 23 +++ .../Scenarios/VertexAttribBindingScenario.cpp | 141 ++++++++++++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 17 ++- .../MG_Util/BackendLoaders/OpenGL/Loader.h | 15 ++ 6 files changed, 281 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index daf38279..d5130cd9 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3591,13 +3591,32 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices); } + // True when the driver will apply baseInstance to the vertex fetch itself, in which case the + // attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal + // and runs either way - it feeds the shader's gl_BaseInstance, not the fetch. + inline Bool UseNativeBaseInstance() { + return g_GLESCapabilities.SupportsBaseInstance; + } + + // The emulated shift has to be in place before PrepareForDraw, because that is what syncs the + // VAO; a zero here is what un-shifts the arrays for the next ordinary draw. + inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) { + return UseNativeBaseInstance() ? 0u : static_cast(baseinstance); + } + void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance)); PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); SetCurrentBaseVertex(basevertex); - g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount, + basevertex, baseinstance); + } else { + g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); + } SetCurrentBaseVertex(0); SetCurrentBaseInstance(0); } @@ -3614,9 +3633,15 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; + const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance)); PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); - g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount, + baseinstance); + } else { + g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount); + } SetCurrentBaseInstance(0); } @@ -3652,9 +3677,14 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) { DrawSyncFlags syncBit = DrawSyncBit::Instancing; + const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance)); PrepareForDraw(syncBit); SetCurrentBaseInstance(baseinstance); - g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); + if (UseNativeBaseInstance()) { + g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance); + } else { + g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount); + } SetCurrentBaseInstance(0); } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 917e6318..ef8e967c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1490,6 +1490,34 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glVertexBindingDivisor != nullptr; } + // Draw state, not VAO state: set by the baseInstance draw entry points around + // PrepareForDraw and back to zero as soon as the draw is issued. + Uint32 g_pendingFetchBaseInstance = 0; + + void SetPendingFetchBaseInstance(Uint32 baseInstance) { + g_pendingFetchBaseInstance = baseInstance; + } + + Uint32 GetPendingFetchBaseInstance() { + return g_pendingFetchBaseInstance; + } + + // The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift + // of the array's own offset. Only divisor'd arrays step per instance, so only they move. + // + // baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor + // therefore does not appear here, and the shift is a whole number of strides. + // + // A resolved stride of zero is the binding model's "never advance" (see + // VertexAttribute::Stride), so such an array reads the same element for every instance + // and a baseInstance cannot move it. The arithmetic already yields zero for that case. + inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) { + if (baseInstance == 0 || attrib.Divisor == 0) { + return 0; + } + return static_cast(baseInstance) * static_cast(attrib.Stride); + } + // Declares one attribute through the ES binding-point API, the only spelling that can // carry a stride of zero. Returns false when the attribute has no usable buffer, in // which case nothing was emitted. @@ -1547,7 +1575,16 @@ namespace MobileGL::MG_Backend::DirectGLES { const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion(); const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion; const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion; - if (!attributesDirty && !indexBufferDirty) { + + // The baseInstance shift lives in the attribute offsets the driver already holds, so + // a change of baseInstance has to re-emit the divisor'd arrays even when the frontend + // config version says nothing moved - and equally has to un-shift them for the next + // draw that carries no baseInstance. Resting state is 0 on both sides, so a program + // that never calls a *BaseInstance entry point never pays for this compare. + const Uint32 fetchBaseInstance = g_pendingFetchBaseInstance; + const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance; + const Bool emitAttributes = attributesDirty || baseInstanceDirty; + if (!emitAttributes && !indexBufferDirty) { return; } @@ -1555,8 +1592,12 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& allAttributeVersions = stateVAOObject->GetAllAttributeVersions(); const auto& allAttributes = stateVAOObject->GetAllAttributes(); - for (Uint attribIndex = 0; attribIndex < allAttributes.size() && attributesDirty; ++attribIndex) { + for (Uint attribIndex = 0; attribIndex < allAttributes.size() && emitAttributes; ++attribIndex) { const auto& attrib = allAttributes[attribIndex]; + // Only the divisor'd arrays carry the shift, and only an enabled one is worth + // re-emitting - a disabled array has no pointer the draw could fetch through, + // and may well have no buffer to bind either. + const Bool needsSyncBaseInstance = baseInstanceDirty && attrib.Enabled && attrib.Divisor != 0; Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion != m_syncedAttributeVersions[attribIndex].SwitchVersion; if (needsSyncSwitch) { @@ -1571,7 +1612,7 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedAttributeVersions[attribIndex].FormatVersion; Bool needsSyncBuffer = allAttributeVersions[attribIndex].BufferVersion != m_syncedAttributeVersions[attribIndex].BufferVersion; - if (!needsSyncFormat && !needsSyncBuffer) continue; + if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue; // Defence in depth. The frontend already declines glVertexAttribLFormat on this // backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex @@ -1611,6 +1652,8 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!SyncZeroStrideAttribute(attribIndex, attrib)) { continue; } + // No BaseInstanceByteShift here on purpose: a zero stride never advances, so + // the shift is zero by construction and adding it would only obscure that. if (needsSyncFormat) { g_GLESFuncs.glVertexBindingDivisor(attribIndex, attrib.Divisor); } @@ -1641,16 +1684,18 @@ namespace MobileGL::MG_Backend::DirectGLES { } // start from a clean slate so the check below is about THIS call } + const SizeT fetchOffset = attrib.Offset + BaseInstanceByteShift(attrib, fetchBaseInstance); + if (!attrib.IsInteger) { // GL_BGRA is passed to the driver as the size argument (the driver reorders BGRA). const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : attrib.Size; g_GLESFuncs.glVertexAttribPointer( attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, (const void*)attrib.Offset); + attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, (const void*)fetchOffset); } else { g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), attrib.Stride, - (const void*)attrib.Offset); + (const void*)fetchOffset); } if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) { @@ -1694,6 +1739,9 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedConfigVersion = currentConfigVersion; m_hasSyncedConfigVersion = true; } + if (emitAttributes) { + m_syncedFetchBaseInstance = fetchBaseInstance; + } } void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays( diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 24750c48..a03ec03b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -466,6 +466,12 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 m_syncedConfigVersion = 0; Array m_syncedAttributeVersions; + // Byte shift currently baked into the instanced arrays' offsets by the baseInstance + // emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it + // is deliberately NOT covered by the config version: the frontend never bumps for it. + // Kept here because it describes what was last EMITTED, which is what the next sync + // has to correct. + Uint32 m_syncedFetchBaseInstance = 0; }; extern StateBackendObjectRegistry @@ -479,6 +485,23 @@ namespace MobileGL::MG_Backend::DirectGLES { void InvalidateVAOBindingCache(); // ES resets the binding to 0 when the currently bound VAO is deleted. void NoteVAOIdDeleted(Uint id); + + // baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an + // instanced array at element "floor(instance / divisor) + baseInstance", and ES has no + // way to say the "+ baseInstance" part - so it is folded into the attribute's own byte + // offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent. + // Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw + // so the next one refetches from element 0; ScopedFetchBaseInstance does both. + void SetPendingFetchBaseInstance(Uint32 baseInstance); + Uint32 GetPendingFetchBaseInstance(); + + class ScopedFetchBaseInstance { + public: + explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); } + ~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); } + ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete; + ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete; + }; } // namespace VertexArrayImpl namespace TextureImpl { diff --git a/MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.cpp index c67b5796..6cac2fc7 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.cpp @@ -136,6 +136,48 @@ void main() { return data; } + // As CapturePoints, but through the baseInstance entry point, and on a capture buffer + // of its own. + // + // Kept separate from CapturePoints rather than defaulting a parameter, for two + // reasons. Every existing caller stays on the draw command that carries no + // baseInstance at all, so the negative control is a DIFFERENT command rather than + // the same one passed a zero. And baseInstance is the first thing here that needs + // several captures in ONE test, which the shared helper cannot currently do: a + // second capture into the same buffer object comes back empty on DirectVulkan + // (respecifying a buffer that is bound to a transform-feedback binding point does + // not reach that binding - reproduced with two plain CapturePoints calls, so it is + // neither about baseInstance nor about this helper). A fresh buffer per capture + // sidesteps it; without that, this scenario would be pinning that bug instead. + std::vector CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount, + GLuint baseInstance, bool useBaseInstanceCommand) { + const std::size_t floats = static_cast(vertexCount) * instanceCount * 16; + std::vector poison(floats, kPoison); + GLuint xfbBuffer = 0; + glGenBuffers(1, &xfbBuffer); + glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer); + glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast(floats * sizeof(float)), poison.data(), + GL_DYNAMIC_DRAW); + + glEnable(GL_RASTERIZER_DISCARD); + glUseProgram(program); + glBeginTransformFeedback(GL_POINTS); + if (useBaseInstanceCommand) { + glDrawArraysInstancedBaseInstance(GL_POINTS, 0, vertexCount, instanceCount, baseInstance); + } else { + glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount); + } + glEndTransformFeedback(); + glDisable(GL_RASTERIZER_DISCARD); + + std::vector data(floats, kPoison); + glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast(floats * sizeof(float)), + data.data()); + glUseProgram(0); + glDeleteBuffers(1, &xfbBuffer); + return data; + } + // point p, attribute a, component c float At(const std::vector& data, int point, int attrib, int component) { const std::size_t index = static_cast(point) * 16 + attrib * 4 + component; @@ -347,6 +389,105 @@ void main() { glDeleteBuffers(1, &vbo); } + // baseInstance moves the ELEMENT the instanced arrays start at. DirectGLES has no + // ES entry point that says so on the drivers we ship against (GL_EXT_base_instance + // is absent on Adreno), so it folds the shift into the attribute's own offset - and + // the thing that made this worth pinning is that the value used to reach the shader + // uniform for gl_BaseInstance and NEVER the fetch, so a draw could report a base + // instance it had not actually read from. + // + // The three draws are the point. Zero first as a negative control, so a backend that + // simply ignored baseInstance could not pass on the middle draw alone; and zero AGAIN + // last, because the shift is emitted into per-attribute state the VAO twin memoises - + // leaving it applied would make every subsequent ordinary draw fetch from the wrong + // element, which is a far worse bug than the one being fixed. + TEST_F(VertexAttribBindingScenario, BaseInstanceMovesTheInstancedArraysStartElement) { + if (!Ready()) GTEST_SKIP(); + ResetCurrentAttribs(); + + const float instanceData[] = {10.0f, 20.0f, 30.0f, 40.0f}; + GLuint vbo = 0; + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(instanceData), instanceData, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0); + glVertexAttribBinding(0, 0); + glBindVertexBuffer(0, vbo, 0, 4); + glVertexBindingDivisor(0, 1); + glEnableVertexAttribArray(0); + + const std::vector atZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true); + EXPECT_TRUE(Vec4Is(atZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(atZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f)); + + const std::vector atTwo = CaptureOwnBufferBaseInstance(m_program, 1, 2, 2, true); + EXPECT_TRUE(Vec4Is(atTwo, 0, 0, 30.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(atTwo, 1, 0, 40.0f, 0.0f, 0.0f, 1.0f)); + + // Nothing about the vertex array changed between these two draws, so only a + // backend that actively un-shifts on a baseInstance change gets back to 10/20. + const std::vector backToZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true); + EXPECT_TRUE(Vec4Is(backToZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(backToZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f)); + + // And a draw command with no baseInstance parameter at all must be unaffected by + // the one that came before it. + const std::vector plain = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, false); + EXPECT_TRUE(Vec4Is(plain, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(plain, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f)); + + glDisableVertexAttribArray(0); + glDeleteBuffers(1, &vbo); + } + + // baseInstance is defined against the instanced arrays only: an array with divisor 0 + // advances per VERTEX and its start element is "first", which baseInstance does not + // touch. An emulation that shifted by offset without checking the divisor would move + // this one too, and nothing in the case above would notice. + TEST_F(VertexAttribBindingScenario, BaseInstanceLeavesPerVertexArraysWhereTheyWere) { + if (!Ready()) GTEST_SKIP(); + ResetCurrentAttribs(); + + const float perVertex[] = {1.0f, 2.0f, 3.0f, 4.0f}; + const float perInstance[] = {10.0f, 20.0f, 30.0f, 40.0f}; + GLuint buffers[2] = {0, 0}; + glGenBuffers(2, buffers); + glBindBuffer(GL_ARRAY_BUFFER, buffers[0]); + glBufferData(GL_ARRAY_BUFFER, sizeof(perVertex), perVertex, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, buffers[1]); + glBufferData(GL_ARRAY_BUFFER, sizeof(perInstance), perInstance, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0); + glVertexAttribBinding(0, 0); + glBindVertexBuffer(0, buffers[0], 0, 4); + glVertexBindingDivisor(0, 0); + glEnableVertexAttribArray(0); + + glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 0); + glVertexAttribBinding(1, 1); + glBindVertexBuffer(1, buffers[1], 0, 4); + glVertexBindingDivisor(1, 1); + glEnableVertexAttribArray(1); + + // 2 vertices x 2 instances, baseInstance 2. Points come out instance-major. + const std::vector data = CaptureOwnBufferBaseInstance(m_program, 2, 2, 2, true); + EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 1, 0, 2.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 2, 0, 1.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 3, 0, 2.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 0, 1, 30.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 1, 1, 30.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_TRUE(Vec4Is(data, 3, 1, 40.0f, 0.0f, 0.0f, 1.0f)); + + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDeleteBuffers(2, buffers); + } + // Two attributes on one binding point at different relative offsets, plus a // binding offset: the fetch address is binding offset + relative offset, and the // relative offset must not leak into the binding's own offset. diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 787d3908..94c470c3 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -540,6 +540,10 @@ namespace MobileGL::MG_Util::BackendLoader { INIT_GLES_FUNC_OPTIONAL(glMultiDrawArraysIndirectEXT) INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsIndirectEXT) INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsBaseVertexEXT) + + INIT_GLES_FUNC_OPTIONAL(glDrawArraysInstancedBaseInstanceEXT) + INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseInstanceEXT) + INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseVertexBaseInstanceEXT) } } @@ -851,6 +855,9 @@ namespace MobileGL::MG_Util::BackendLoader { // Resolved into caps.TextureBufferSupport below, once the ES version is also known. Bool hasExtTextureBuffer = false; Bool hasOesTextureBuffer = false; + // Combined with the three entry points below; DirectGLES emulates baseInstance when this + // comes out false, so a stub pointer counting as support would silently break the draws. + Bool hasBaseInstanceExtension = false; for (GLint i = 0; i < extCount; ++i) { const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i); if (extension) { @@ -891,7 +898,7 @@ namespace MobileGL::MG_Util::BackendLoader { hasOesTextureBuffer = true; } if (std::strcmp(extension, "GL_EXT_base_instance") == 0) { - caps.SupportsBaseInstance = true; + hasBaseInstanceExtension = true; } if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) { caps.SupportsDisjointTimerQuery = true; @@ -929,6 +936,12 @@ namespace MobileGL::MG_Util::BackendLoader { caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension && hasMultiDrawArraysExtension && glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr; + // All three, not any: DirectGLES picks native-vs-emulated once per draw entry point off + // this single flag, so a driver that resolved only some of them must count as absent. + caps.SupportsBaseInstance = hasBaseInstanceExtension && + glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr && + glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr && + glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr; // Core from ES 3.2 on, so an extension string is not required there; below 3.2 the // extension is, and the pointer still has to have resolved either way. const Bool esAtLeast32 = caps.GLESVersion.Major > 3 || @@ -963,6 +976,8 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" draw elements base vertex (ES 3.2 core or EXT/OES_draw_elements_base_vertex): %s", caps.SupportsDrawElementsBaseVertex ? "yes" : "no"); MGLOG_I(" compute shaders (ES 3.1 core): %s", caps.SupportsComputeShader ? "yes" : "no"); + MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s", + caps.SupportsBaseInstance ? "yes" : "no"); MGLOG_I("OpenGL ES capabilities:"); glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 2b13941b..ef362704 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -638,6 +638,17 @@ namespace MobileGL { GL_FUNC_TYPEDEF(void, glBruh) GL_FUNC_TYPEDEF(void, glMultiDrawElementsBaseVertexEXT, GLenum mode, const GLsizei* count, GLenum type, const void* const* indices, GLsizei drawcount, const GLint* basevertex) + + // GL_EXT_base_instance. Where a driver has these, the "+ baseInstance" of the + // instanced-array element index is the driver's job; where it does not, DirectGLES + // folds it into the attribute offsets instead (VertexArrayImpl::BaseInstanceByteShift). + GL_FUNC_TYPEDEF(void, glDrawArraysInstancedBaseInstanceEXT, GLenum mode, GLint first, GLsizei count, + GLsizei instancecount, GLuint baseinstance) + GL_FUNC_TYPEDEF(void, glDrawElementsInstancedBaseInstanceEXT, GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, GLuint baseinstance) + GL_FUNC_TYPEDEF(void, glDrawElementsInstancedBaseVertexBaseInstanceEXT, GLenum mode, GLsizei count, + GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, + GLuint baseinstance) /* namespace Caps { struct GLESCaps { @@ -1034,6 +1045,10 @@ namespace MobileGL { GL_FUNC_DECL(glMultiDrawElementsIndirectEXT) GL_FUNC_DECL(glMultiDrawElementsBaseVertexEXT) + GL_FUNC_DECL(glDrawArraysInstancedBaseInstanceEXT) + GL_FUNC_DECL(glDrawElementsInstancedBaseInstanceEXT) + GL_FUNC_DECL(glDrawElementsInstancedBaseVertexBaseInstanceEXT) + GL_FUNC_DECL(glBruh) };