diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index b4b2459a..583d7a74 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -49,6 +49,7 @@ add_executable(MobileGLIntegrationTest Scenarios/OrientationScenario.cpp Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp + Scenarios/MultiDrawScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.cpp new file mode 100644 index 00000000..14bb5265 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.cpp @@ -0,0 +1,522 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.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 +// +// Scenario D - glMultiDrawElements(BaseVertex) against the draws it stands for. +// +// Neither entry point exists in OpenGL ES, so DirectGLES emulates both through a +// ladder of tiers (MG_Backend/DirectGLES/MultiDraw.cpp): a native +// glMultiDrawElementsBaseVertexEXT, synthesized indirect commands drawn one at a +// time or in one batch, a per-sub-draw replay, a CPU rewrite of the index stream, +// and a compute shader that flattens the whole batch into a single draw. They +// share nothing but their contract, which is the one thing asserted here: +// +// a multi-draw must paint exactly what the unrolled single draws paint. +// +// The reference side never enters the emulation - it is a loop of +// glDrawElementsBaseVertex / glDrawElements - so a tier cannot make itself look +// right by breaking both sides the same way. +// +// The Minecraft retraces already cover the common shape (GL_UNSIGNED_INT indices +// in a bound element array buffer, small base vertices, GL_TRIANGLES) on every +// tier. What they contain none of, and what these cases are for, is the set of +// shapes where a tier has to decline or compensate rather than replay: +// +// * narrow index types, where a rewritten stream has to widen (BYTE/SHORT); +// * a base vertex past the index type's range, where folding it into the +// indices at the source width silently wraps - GL adds base vertices at full +// precision, so `ushort index 10 + baseVertex 70000` is vertex 70010 and not +// vertex 4474; +// * primitive restart, where a rewritten stream must carry the sentinel across +// unrebased or the restart is lost and the strip welds shut; +// * client-memory index arrays, which have no buffer for the indirect tiers to +// address or for the compute tier to read; +// * a strip mode, which the flattening tier must decline outright because +// concatenation would weld one sub-draw's last primitive to the next +// sub-draw's first. +// +// One process is one tier (MOBILEGL_ESPRYT_MULTIDRAW_MODE is read once at +// startup), so a single run exercises whichever tier this driver resolved to. +// Running the binary once per mode is what covers the ladder; each run is a +// complete, self-contained proof for the tier it landed on. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include + +namespace MGITest { + namespace { + + constexpr const char* kVertexSource = R"(#version 330 core +layout(location = 0) in vec2 aPos; +layout(location = 1) in vec3 aColor; +out vec3 vColor; +void main() { + vColor = aColor; + gl_Position = vec4(aPos, 0.0, 1.0); +} +)"; + + constexpr const char* kFragmentSource = R"(#version 330 core +in vec3 vColor; +out vec4 oColor; +void main() { + oColor = vec4(vColor, 1.0); +} +)"; + + struct Vertex { + float x, y; + float r, g, b; + }; + + // Four column quads spanning the viewport left to right, in four colours, + // so a sub-draw that lands in the wrong place, draws the wrong vertices or + // does not draw at all changes the picture rather than hiding inside it. + constexpr int kColumns = 4; + + const Rgba8 kColumnColors[kColumns] = { + {255, 0, 0, 255}, + {0, 255, 0, 255}, + {0, 0, 255, 255}, + {255, 255, 255, 255}, + }; + + // `padVertices` leading dummies force every sub-draw to need its own base + // vertex: without one applied, a draw reads the padding and paints black. + std::vector ColumnVertices(int padVertices) { + std::vector vertices(static_cast(padVertices), Vertex{0.0f, 0.0f, 0.0f, 0.0f, 0.0f}); + for (int column = 0; column < kColumns; ++column) { + const float x0 = -1.0f + 2.0f * static_cast(column) / kColumns; + const float x1 = -1.0f + 2.0f * static_cast(column + 1) / kColumns; + const Rgba8 color = kColumnColors[column]; + const float r = color.r / 255.0f; + const float g = color.g / 255.0f; + const float b = color.b / 255.0f; + vertices.push_back({x0, -1.0f, r, g, b}); + vertices.push_back({x1, -1.0f, r, g, b}); + vertices.push_back({x1, 1.0f, r, g, b}); + vertices.push_back({x0, 1.0f, r, g, b}); + } + return vertices; + } + + // Every sub-draw uses the SAME six indices, 0..3 relative to its own quad; + // only the base vertex tells the columns apart. That makes the base vertex + // the load-bearing part of the batch. + const std::uint32_t kQuadIndices[6] = {0, 1, 2, 0, 2, 3}; + + // One column, as a restart-separated pair of triangle strips. Two strips in + // one sub-draw means the sentinel is genuinely interior: drop it and the two + // halves weld into a single strip that paints across the gap between them. + // Indices are relative to the sub-draw's own quad, like kQuadIndices. + template + std::vector RestartStripIndices(Index restartSentinel) { + // 3,0,2,1 is the strip winding of the quad; splitting it around the + // sentinel gives two degenerate-free halves that redraw the same area. + return {Index{3}, Index{0}, Index{2}, restartSentinel, Index{0}, Index{2}, Index{1}}; + } + + class MultiDrawScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + std::string error; + m_program = CompileProgram(kVertexSource, kFragmentSource, &error); + ASSERT_NE(m_program, 0u) << error; + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "program setup left a GL error behind"; + } + + void TearDown() override { + if (!Ready()) return; + ReleaseBuffers(); + if (m_program != 0) glDeleteProgram(m_program); + } + + // VAO + VBO, and an EBO only when `indexBytes` is non-null: a null one + // leaves GL_ELEMENT_ARRAY_BUFFER unbound so the sub-draws address client + // memory, which is the shape that forces the buffer-reading tiers out. + void BuildScene(int padVertices, const void* indexBytes, std::size_t indexByteCount) { + ReleaseBuffers(); + const std::vector vertices = ColumnVertices(padVertices); + + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + glGenBuffers(1, &m_vbo); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, static_cast(vertices.size() * sizeof(Vertex)), + vertices.data(), GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(0)); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(sizeof(float) * 2)); + + if (indexBytes != nullptr) { + glGenBuffers(1, &m_ebo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast(indexByteCount), indexBytes, + GL_STATIC_DRAW); + } + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind"; + } + + void ReleaseBuffers() { + if (m_ebo != 0) glDeleteBuffers(1, &m_ebo); + if (m_vbo != 0) glDeleteBuffers(1, &m_vbo); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + m_ebo = 0; + m_vbo = 0; + m_vao = 0; + } + + GLuint m_program = 0; + GLuint m_vao = 0; + GLuint m_vbo = 0; + GLuint m_ebo = 0; + }; + + // Runs `draw`, reads the default framebuffer back and returns the image. + template + Image RenderPass(GLuint program, GLuint vao, DrawFn&& draw) { + BindDefaultFramebuffer(); + glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height()); + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + glUseProgram(program); + glBindVertexArray(vao); + draw(); + return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height()); + } + + // The whole point of the file: two renderings of the same geometry, one + // through the multi-draw emulation and one through the single-draw entry + // points it stands for, must be identical to the byte. + void ExpectSameImage(const Image& multiDraw, const Image& unrolled, const std::string& what) { + ASSERT_FALSE(multiDraw.Empty()) << what << ": multi-draw readback was empty"; + ASSERT_FALSE(unrolled.Empty()) << what << ": reference readback was empty"; + EXPECT_EQ(multiDraw, unrolled) + << what << ": glMultiDraw* painted something else than the draws it stands for (" + << multiDraw.ByteDiffCount(unrolled) << " bytes differ; multi-draw quadrants " + << multiDraw.QuadrantSignature() << ", unrolled quadrants " << unrolled.QuadrantSignature() << ")"; + // A pair of blank frames would satisfy the comparison above and prove + // nothing at all - the failure mode a multi-draw path most often has is + // drawing NOTHING (see the shipped glMultiDrawElementsBaseVertexEXT stub + // that silently dropped every draw). Demand the columns really landed. + EXPECT_NE(multiDraw.QuadrantSignature(), "black,black,black,black") << what << ": nothing was drawn at all"; + } + + // ---- GL_UNSIGNED_INT indices in a buffer, per-sub-draw base vertices ---- + + TEST_F(MultiDrawScenario, BaseVertexBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 5; // odd, so nothing lines up by accident + BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "GL_UNSIGNED_INT indices, per-sub-draw base vertices"); + } + + // ---- glMultiDrawElements: no base vertices, distinct index offsets ---- + + TEST_F(MultiDrawScenario, PlainBatchMatchesUnrolledDraws) { + if (!Ready()) return; + // No padding and no base vertices: each sub-draw reaches its own column + // through its index offset instead. + std::vector indices; + for (int column = 0; column < kColumns; ++column) { + for (const std::uint32_t index : kQuadIndices) { + indices.push_back(index + static_cast(column * 4)); + } + } + BuildScene(0, indices.data(), indices.size() * sizeof(std::uint32_t)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = reinterpret_cast(static_cast(i * 6 * sizeof(std::uint32_t))); + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElements(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElements(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "glMultiDrawElements with no base vertices"); + } + + // ---- narrow index types ---- + // A tier that rewrites the stream emits GL_UNSIGNED_INT whatever came in, + // so these two say the widening reproduces the original draw exactly. + + TEST_F(MultiDrawScenario, UnsignedShortBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 3; + std::uint16_t indices[6]; + for (int i = 0; i < 6; ++i) + indices[i] = static_cast(kQuadIndices[i]); + BuildScene(kPad, indices, sizeof(indices)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "GL_UNSIGNED_SHORT indices"); + } + + TEST_F(MultiDrawScenario, UnsignedByteBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 3; + std::uint8_t indices[6]; + for (int i = 0; i < 6; ++i) + indices[i] = static_cast(kQuadIndices[i]); + // 24 bytes: a word multiple, which the compute tier needs of the source + // buffer when the index type is narrower than a word. + std::uint8_t padded[24] = {}; + for (int i = 0; i < 6; ++i) + padded[i] = indices[i]; + BuildScene(kPad, padded, sizeof(padded)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_BYTE, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_BYTE, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "GL_UNSIGNED_BYTE indices"); + } + + // ---- a base vertex the index type cannot spell ---- + // GL adds the base vertex at full precision, so folding it into a + // GL_UNSIGNED_SHORT index stream at the source width wraps and addresses the + // wrong vertex. The columns here start past 65535, which no ushort index can + // reach on its own. + + TEST_F(MultiDrawScenario, BaseVertexBeyondIndexTypeRangeMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 70000; // > 0xFFFF + std::uint16_t indices[6]; + for (int i = 0; i < 6; ++i) + indices[i] = static_cast(kQuadIndices[i]); + BuildScene(kPad, indices, sizeof(indices)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "base vertex past the GL_UNSIGNED_SHORT range"); + } + + // ---- client-memory index arrays ---- + // No element array buffer, so the indirect tiers have nothing to address and + // the compute tier nothing to read; both must decline and hand the batch to + // a tier that can replay it. + + TEST_F(MultiDrawScenario, ClientSideIndicesBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 5; + BuildScene(kPad, nullptr, 0); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = 6; + offsets[i] = kQuadIndices; + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "client-memory index arrays"); + } + + // ---- primitive restart inside a strip ---- + // Two things at once: a strip mode, which the flattening tier must decline + // because concatenation would weld sub-draws together, and a restart + // sentinel, which any tier that rewrites indices must carry across without + // adding the base vertex to it. + + TEST_F(MultiDrawScenario, PrimitiveRestartStripBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 5; + const std::vector indices = RestartStripIndices(0xFFFFFFFFu); + BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint32_t)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = static_cast(indices.size()); + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, kColumns, + baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_INT, offsets[i], + baseVertices[i]); + } + }); + glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with primitive restart"); + } + + // Same, with GL_UNSIGNED_SHORT: the sentinel a rewritten stream has to + // recognise is the index TYPE's all-ones value, not the rewritten stream's. + TEST_F(MultiDrawScenario, PrimitiveRestartUnsignedShortBatchMatchesUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 5; + const std::vector indices = RestartStripIndices(0xFFFFu); + BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint16_t)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + counts[i] = static_cast(indices.size()); + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_SHORT, offsets, kColumns, + baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_SHORT, offsets[i], + baseVertices[i]); + } + }); + glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with GL_UNSIGNED_SHORT primitive restart"); + } + + // ---- a batch with holes ---- + // Zero-count sub-draws draw nothing. The flattening tier's binary search + // finds a sub-draw by prefix sum, and a zero-count entry repeats the + // previous sum - so a search that resolves ties the other way would attribute + // indices to the empty draw and paint the wrong column. + + TEST_F(MultiDrawScenario, ZeroCountSubDrawsMatchUnrolledDraws) { + if (!Ready()) return; + constexpr int kPad = 5; + BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices)); + + GLsizei counts[kColumns]; + const void* offsets[kColumns]; + GLint baseVertices[kColumns]; + for (int i = 0; i < kColumns; ++i) { + // Columns 1 and 2 are skipped, leaving the outer two painted. + counts[i] = (i == 1 || i == 2) ? 0 : 6; + offsets[i] = reinterpret_cast(0); + baseVertices[i] = kPad + i * 4; + } + + const Image batched = RenderPass(m_program, m_vao, [&] { + glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices); + }); + const Image unrolled = RenderPass(m_program, m_vao, [&] { + for (int i = 0; i < kColumns; ++i) { + if (counts[i] == 0) continue; + glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]); + } + }); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws"); + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 2b7bcc19..7bd7a39f 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include // Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no // MG_State code: it runs standalone, before MG_State::Init(). @@ -319,9 +320,46 @@ namespace MobileGL::MG_Util::SelfTest { } else { builder.Info("Multi-draw base vertex", "glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_" - "draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); " - "glMultiDrawElementsBaseVertex falls back to a per-draw loop with " - "identical output"); + "draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch " + "takes the next emulation tier instead, with identical output - see " + "\"Multi-draw elements tier\" below for the one that will run"); + } + // glMultiDrawElements(BaseVertex) has no ES counterpart at all, so DirectGLES + // emulates it; these rows say which emulation the driver leaves available and + // which one will run. The two capabilities each tier leans on come first. + if (caps.SupportsDrawElementsBaseVertex) { + builder.Pass("Draw elements base vertex", + "glDrawElementsBaseVertex available (ES 3.2 core or EXT/OES_draw_elements_base_" + "vertex); a multi-draw batch can replay its sub-draws with their own base " + "vertices"); + } else { + builder.Warn("Draw elements base vertex", + "glDrawElementsBaseVertex not supported (pre-ES 3.2 without EXT/OES_draw_" + "elements_base_vertex); every base-vertex draw has to be emulated by rewriting " + "the index stream on the CPU, which costs an upload per batch"); + } + if (caps.SupportsComputeShader) { + builder.Pass("Compute shaders", + "available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a " + "whole batch into one draw"); + } else { + builder.Info("Compute shaders", + "not available (pre-ES 3.1); no impact on the default multi-draw tiers, which " + "never use compute"); + } + { + // The same resolution the backend runs, over the capabilities probed here. + // Like the Magma tier row, the preference comes from MG_Config::Features, + // which is only populated once MobileGL::Initialize() has parsed the + // environment - a POST executed standalone before that reports the + // unclamped choice, so the row names the variable rather than implying it + // was consulted. + using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier; + String resolution; + ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution); + builder.Info("Multi-draw elements tier", + "glMultiDrawElements(BaseVertex) emulation: " + resolution + + "; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE"); } if (caps.SupportsTextureBorderClamp) { builder.Pass("Texture border clamp",