[Fix, Test] (MG_Backend/DirectGLES, MG_Util): baseInstance reached the shader but never the vertex fetch

This commit is contained in:
2026-08-12 08:12:27 -04:00
parent ccad803023
commit 1ebf191f94
6 changed files with 281 additions and 9 deletions
+33 -3
View File
@@ -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<Uint32>(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);
}
+53 -5
View File
@@ -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<SizeT>(baseInstance) * static_cast<SizeT>(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<GLint>(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(
+23
View File
@@ -466,6 +466,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 m_syncedConfigVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
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<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
@@ -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 {
@@ -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<float> CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount,
GLuint baseInstance, bool useBaseInstanceCommand) {
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
std::vector<float> poison(floats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(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<float> data(floats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
data.data());
glUseProgram(0);
glDeleteBuffers(1, &xfbBuffer);
return data;
}
// point p, attribute a, component c
float At(const std::vector<float>& data, int point, int attrib, int component) {
const std::size_t index = static_cast<std::size_t>(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<float> 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<float> 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<float> 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<float> 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<float> 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.
@@ -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);
@@ -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)
};