diff --git a/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp new file mode 100644 index 00000000..ff5adb4a --- /dev/null +++ b/MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp @@ -0,0 +1,402 @@ +// MobileGL - MobileGL/MG_Test/BackendLoader/BackendLoaderTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include +#include +#include +#include + +#include + +// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver: +// a GLESFunctionsTable populated with captureless lambdas backed by the file-scope +// state below (buffer stores, bound targets, always-succeeding compile/link). Each +// test configures the fake's draw behavior to emulate a conforming driver, an +// ANGLE-style baseInstance-leaking driver, or a failing one. +namespace { + struct FakeDriverState { + // Behavior knobs, configured per test before running the probe. + GLint maxVertexSsboBlocks = 4; + // Emulates ANGLE-on-Vulkan: the draw reads the indirect command's + // baseInstance word and exposes it through gl_InstanceID. + bool drawLeaksBaseInstanceWord = false; + GLenum errorRaisedByDraw = GL_NO_ERROR; + + GLenum pendingError = GL_NO_ERROR; + + GLuint nextBufferId = 1; + GLuint nextShaderId = 1; + GLuint nextProgramId = 1; + GLuint nextVertexArrayId = 1; + GLuint nextFramebufferId = 1; + GLuint nextRenderbufferId = 1; + + std::map> bufferStores; // buffer id -> data store + std::map boundBuffers; // target -> buffer id + std::map boundSsboBases; // SSBO binding index -> buffer id + + int createdShaders = 0; + int createdPrograms = 0; + int createdBuffers = 0; + int createdVertexArrays = 0; + int createdFramebuffers = 0; + int createdRenderbuffers = 0; + int aliveShaders = 0; + int alivePrograms = 0; + int aliveBuffers = 0; + int aliveVertexArrays = 0; + int aliveFramebuffers = 0; + int aliveRenderbuffers = 0; + + bool drawIssued = false; + }; + + FakeDriverState g_fake; + + void ResetFakeDriver() { g_fake = FakeDriverState{}; } + + std::vector* StoreOfBufferBoundTo(GLenum target) { + const auto boundIt = g_fake.boundBuffers.find(target); + if (boundIt == g_fake.boundBuffers.end() || boundIt->second == 0) { + return nullptr; + } + const auto storeIt = g_fake.bufferStores.find(boundIt->second); + return storeIt != g_fake.bufferStores.end() ? &storeIt->second : nullptr; + } + + MobileGL::MG_External::GLESFunctionsTable MakeFakeGLESFunctions() { + MobileGL::MG_External::GLESFunctionsTable funcs{}; + + funcs.glGetIntegerv = [](GLenum pname, GLint* data) { + switch (pname) { + case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: + *data = g_fake.maxVertexSsboBlocks; + break; + // FillInGLESCapabilities reads the context version before running the + // baseInstance probe, which requires ES >= 3.1. + case GL_MAJOR_VERSION: + *data = 3; + break; + case GL_MINOR_VERSION: + *data = 1; + break; + case GL_NUM_EXTENSIONS: + *data = 0; + break; + default: + // Leave the caller's defaults for every other capability query. + break; + } + }; + funcs.glGetError = []() -> GLenum { + const GLenum error = g_fake.pendingError; + g_fake.pendingError = GL_NO_ERROR; + return error; + }; + + // String and float queries used by FillInGLESCapabilities. + funcs.glGetString = [](GLenum name) -> const GLubyte* { + switch (name) { + case GL_VENDOR: + return reinterpret_cast("MobileGL Fake Vendor"); + case GL_RENDERER: + return reinterpret_cast("MobileGL Fake Renderer"); + case GL_VERSION: + return reinterpret_cast("OpenGL ES 3.1 (MobileGL fake)"); + case GL_SHADING_LANGUAGE_VERSION: + return reinterpret_cast("OpenGL ES GLSL ES 3.10 (MobileGL fake)"); + default: + return reinterpret_cast(""); + } + }; + // GL_NUM_EXTENSIONS reports 0 above, so this is never reached; it exists so the + // table stays complete if the extension loop ever runs. + funcs.glGetStringi = [](GLenum, GLuint) -> const GLubyte* { return nullptr; }; + funcs.glGetFloatv = [](GLenum pname, GLfloat* data) { + switch (pname) { + // Two-component range queries. + case GL_ALIASED_LINE_WIDTH_RANGE: + case GL_SMOOTH_LINE_WIDTH_RANGE: + case GL_ALIASED_POINT_SIZE_RANGE: + case GL_VIEWPORT_BOUNDS_RANGE: + data[0] = 0.0f; + data[1] = 0.0f; + break; + default: + data[0] = 0.0f; + break; + } + }; + + // Shader and program objects: compile/link always succeed. + funcs.glCreateShader = [](GLenum) -> GLuint { + ++g_fake.createdShaders; + ++g_fake.aliveShaders; + return g_fake.nextShaderId++; + }; + funcs.glShaderSource = [](GLuint, GLsizei, const GLchar* const*, const GLint*) {}; + funcs.glCompileShader = [](GLuint) {}; + funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) { + if (pname == GL_COMPILE_STATUS) { + *params = GL_TRUE; + } + }; + funcs.glDeleteShader = [](GLuint shader) { + if (shader != 0) { + --g_fake.aliveShaders; + } + }; + funcs.glCreateProgram = []() -> GLuint { + ++g_fake.createdPrograms; + ++g_fake.alivePrograms; + return g_fake.nextProgramId++; + }; + funcs.glAttachShader = [](GLuint, GLuint) {}; + funcs.glLinkProgram = [](GLuint) {}; + funcs.glGetProgramiv = [](GLuint, GLenum pname, GLint* params) { + if (pname == GL_LINK_STATUS) { + *params = GL_TRUE; + } + }; + funcs.glDeleteProgram = [](GLuint program) { + if (program != 0) { + --g_fake.alivePrograms; + } + }; + funcs.glUseProgram = [](GLuint) {}; + + // Buffer objects with byte-accurate data stores. + funcs.glGenBuffers = [](GLsizei n, GLuint* buffers) { + for (GLsizei i = 0; i < n; ++i) { + buffers[i] = g_fake.nextBufferId++; + ++g_fake.createdBuffers; + ++g_fake.aliveBuffers; + } + }; + funcs.glBindBuffer = [](GLenum target, GLuint buffer) { g_fake.boundBuffers[target] = buffer; }; + funcs.glBufferData = [](GLenum target, GLsizeiptr size, const void* data, GLenum) { + const GLuint bound = g_fake.boundBuffers[target]; + if (bound == 0) { + return; + } + auto& store = g_fake.bufferStores[bound]; + store.assign((std::size_t)size, 0); + if (data != nullptr && size > 0) { + std::memcpy(store.data(), data, (std::size_t)size); + } + }; + funcs.glBindBufferBase = [](GLenum target, GLuint index, GLuint buffer) { + if (target == GL_SHADER_STORAGE_BUFFER) { + g_fake.boundSsboBases[index] = buffer; + } + }; + funcs.glDeleteBuffers = [](GLsizei n, const GLuint* buffers) { + for (GLsizei i = 0; i < n; ++i) { + if (buffers[i] != 0) { + --g_fake.aliveBuffers; + g_fake.bufferStores.erase(buffers[i]); + } + } + }; + funcs.glMapBufferRange = [](GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield) -> void* { + auto* store = StoreOfBufferBoundTo(target); + if (store == nullptr || offset < 0 || (std::size_t)(offset + length) > store->size()) { + return nullptr; + } + return store->data() + offset; + }; + funcs.glUnmapBuffer = [](GLenum) -> GLboolean { return GL_TRUE; }; + + // Vertex array objects. + funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) { + for (GLsizei i = 0; i < n; ++i) { + arrays[i] = g_fake.nextVertexArrayId++; + ++g_fake.createdVertexArrays; + ++g_fake.aliveVertexArrays; + } + }; + funcs.glBindVertexArray = [](GLuint) {}; + funcs.glDeleteVertexArrays = [](GLsizei n, const GLuint* arrays) { + for (GLsizei i = 0; i < n; ++i) { + if (arrays[i] != 0) { + --g_fake.aliveVertexArrays; + } + } + }; + + // Framebuffer/renderbuffer objects for the probe's 1x1 draw target. + funcs.glGenFramebuffers = [](GLsizei n, GLuint* framebuffers) { + for (GLsizei i = 0; i < n; ++i) { + framebuffers[i] = g_fake.nextFramebufferId++; + ++g_fake.createdFramebuffers; + ++g_fake.aliveFramebuffers; + } + }; + funcs.glGenRenderbuffers = [](GLsizei n, GLuint* renderbuffers) { + for (GLsizei i = 0; i < n; ++i) { + renderbuffers[i] = g_fake.nextRenderbufferId++; + ++g_fake.createdRenderbuffers; + ++g_fake.aliveRenderbuffers; + } + }; + funcs.glBindFramebuffer = [](GLenum, GLuint) {}; + funcs.glBindRenderbuffer = [](GLenum, GLuint) {}; + funcs.glRenderbufferStorage = [](GLenum, GLenum, GLsizei, GLsizei) {}; + funcs.glFramebufferRenderbuffer = [](GLenum, GLenum, GLenum, GLuint) {}; + funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) { + for (GLsizei i = 0; i < n; ++i) { + if (framebuffers[i] != 0) { + --g_fake.aliveFramebuffers; + } + } + }; + funcs.glDeleteRenderbuffers = [](GLsizei n, const GLuint* renderbuffers) { + for (GLsizei i = 0; i < n; ++i) { + if (renderbuffers[i] != 0) { + --g_fake.aliveRenderbuffers; + } + } + }; + + funcs.glEnable = [](GLenum) {}; + funcs.glDisable = [](GLenum) {}; + funcs.glMemoryBarrier = [](GLbitfield) {}; + + // The probe's vertex shader writes the gl_InstanceID it observed into the + // result SSBO at binding 0. A conforming driver observes 0; a leaking one + // observes the indirect command's baseInstance word (byte offset 12). + funcs.glDrawArraysIndirect = [](GLenum, const void*) { + g_fake.drawIssued = true; + GLint observedInstanceId = 0; + if (g_fake.drawLeaksBaseInstanceWord) { + const auto* command = StoreOfBufferBoundTo(GL_DRAW_INDIRECT_BUFFER); + if (command != nullptr && command->size() >= 16) { + GLuint baseInstance = 0; + std::memcpy(&baseInstance, command->data() + 12, sizeof(baseInstance)); + observedInstanceId = (GLint)baseInstance; + } + } + const auto resultIt = g_fake.boundSsboBases.find(0); + if (resultIt != g_fake.boundSsboBases.end()) { + const auto storeIt = g_fake.bufferStores.find(resultIt->second); + if (storeIt != g_fake.bufferStores.end() && storeIt->second.size() >= sizeof(observedInstanceId)) { + std::memcpy(storeIt->second.data(), &observedInstanceId, sizeof(observedInstanceId)); + } + } + if (g_fake.errorRaisedByDraw != GL_NO_ERROR) { + g_fake.pendingError = g_fake.errorRaisedByDraw; + } + }; + + return funcs; + } + + MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() { + MobileGL::MG_External::GLESCapabilities caps; + caps.GLESVersion = {3, 1, 0}; + return caps; + } + + void ExpectProbeReleasedAllObjects() { + EXPECT_GT(g_fake.createdShaders, 0); + EXPECT_GT(g_fake.createdPrograms, 0); + EXPECT_GT(g_fake.createdBuffers, 0); + EXPECT_GT(g_fake.createdVertexArrays, 0); + EXPECT_EQ(g_fake.aliveShaders, 0); + EXPECT_EQ(g_fake.alivePrograms, 0); + EXPECT_EQ(g_fake.aliveBuffers, 0); + EXPECT_EQ(g_fake.aliveVertexArrays, 0); + EXPECT_EQ(g_fake.aliveFramebuffers, 0); + EXPECT_EQ(g_fake.aliveRenderbuffers, 0); + EXPECT_TRUE(g_fake.bufferStores.empty()); + } +} // namespace + +TEST(IndirectInstanceIdProbe, ConformingDriverReportsZeroBased) { + ResetFakeDriver(); + const auto funcs = MakeFakeGLESFunctions(); + const auto caps = MakeEs31Capabilities(); + + EXPECT_FALSE(MobileGL::MG_Util::BackendLoader::ProbeIndirectInstanceIdIncludesBaseInstance(caps, funcs)); + + EXPECT_TRUE(g_fake.drawIssued); + ExpectProbeReleasedAllObjects(); +} + +TEST(IndirectInstanceIdProbe, LeakingDriverReportsIncludesBase) { + ResetFakeDriver(); + g_fake.drawLeaksBaseInstanceWord = true; + const auto funcs = MakeFakeGLESFunctions(); + const auto caps = MakeEs31Capabilities(); + + EXPECT_TRUE(MobileGL::MG_Util::BackendLoader::ProbeIndirectInstanceIdIncludesBaseInstance(caps, funcs)); + + EXPECT_TRUE(g_fake.drawIssued); + ExpectProbeReleasedAllObjects(); +} + +TEST(IndirectInstanceIdProbe, NoVertexSsboSkipsProbe) { + ResetFakeDriver(); + g_fake.maxVertexSsboBlocks = 0; + const auto funcs = MakeFakeGLESFunctions(); + const auto caps = MakeEs31Capabilities(); + + EXPECT_FALSE(MobileGL::MG_Util::BackendLoader::ProbeIndirectInstanceIdIncludesBaseInstance(caps, funcs)); + + EXPECT_FALSE(g_fake.drawIssued); + EXPECT_EQ(g_fake.createdBuffers, 0); + EXPECT_EQ(g_fake.createdPrograms, 0); +} + +TEST(IndirectInstanceIdProbe, DrawErrorIsInconclusive) { + ResetFakeDriver(); + // Even when the driver would leak baseInstance, a draw that raises a GL error + // must leave the probe inconclusive (false) instead of trusting the result. + g_fake.drawLeaksBaseInstanceWord = true; + g_fake.errorRaisedByDraw = GL_INVALID_OPERATION; + const auto funcs = MakeFakeGLESFunctions(); + const auto caps = MakeEs31Capabilities(); + + EXPECT_FALSE(MobileGL::MG_Util::BackendLoader::ProbeIndirectInstanceIdIncludesBaseInstance(caps, funcs)); + + EXPECT_TRUE(g_fake.drawIssued); + ExpectProbeReleasedAllObjects(); +} + +// End-to-end through the real capability query: FillInGLESCapabilities must run the +// baseInstance probe against the driver it was handed and store the answer in +// caps.IndirectDrawInstanceIdIncludesBaseInstance (the single call site in Loader.cpp). +TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) { + // Leaking fake: the probe's true result must land in the caps struct. + ResetFakeDriver(); + g_fake.drawLeaksBaseInstanceWord = true; + const auto funcs = MakeFakeGLESFunctions(); + + MobileGL::MG_External::GLESCapabilities leakingCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(leakingCaps, funcs)); + + EXPECT_TRUE(g_fake.drawIssued); + EXPECT_TRUE(leakingCaps.IndirectDrawInstanceIdIncludesBaseInstance); + // The surrounding wiring came from the fake driver too. + EXPECT_EQ(leakingCaps.GLESVersion.Major, 3); + EXPECT_EQ(leakingCaps.GLESVersion.Minor, 1); + EXPECT_EQ(leakingCaps.GLESVendorString, "MobileGL Fake Vendor"); + EXPECT_EQ(leakingCaps.GLESRendererString, "MobileGL Fake Renderer"); + EXPECT_EQ(leakingCaps.GLESVersionString, "OpenGL ES 3.1 (MobileGL fake)"); + EXPECT_EQ(leakingCaps.GLESShadingLanguageVersionString, "OpenGL ES GLSL ES 3.10 (MobileGL fake)"); + ExpectProbeReleasedAllObjects(); + + // Conforming fake: the same call site must record false. + ResetFakeDriver(); + MobileGL::MG_External::GLESCapabilities conformingCaps; + ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(conformingCaps, funcs)); + + EXPECT_TRUE(g_fake.drawIssued); + EXPECT_FALSE(conformingCaps.IndirectDrawInstanceIdIncludesBaseInstance); + ExpectProbeReleasedAllObjects(); +} diff --git a/MobileGL/MG_Test/BackendLoader/CMakeLists.txt b/MobileGL/MG_Test/BackendLoader/CMakeLists.txt new file mode 100644 index 00000000..60a85064 --- /dev/null +++ b/MobileGL/MG_Test/BackendLoader/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.14) + +add_executable( + BackendLoaderTest + BackendLoaderTest.cpp +) + +target_include_directories(BackendLoaderTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL +) + +target_link_libraries( + BackendLoaderTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + +include(GoogleTest) +gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30) diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index d69298ea..fde0a937 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -64,6 +64,7 @@ set(LINK_LIBRARIES include(GoogleTest) gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30) +add_subdirectory(BackendLoader) add_subdirectory(Buffer) add_subdirectory(EGLState) add_subdirectory(Framebuffer) diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index c5cc3bfa..498258f9 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -77,6 +77,31 @@ namespace { unsetenv(name); #endif } + + MobileGL::SizeT CountOccurrences(const MobileGL::String& haystack, const MobileGL::String& needle) { + MobileGL::SizeT count = 0; + for (MobileGL::SizeT pos = haystack.find(needle); pos != MobileGL::String::npos; + pos = haystack.find(needle, pos + needle.size())) { + ++count; + } + return count; + } + + // Snapshots the DirectGLES capability globals on construction and restores them on + // destruction, so tests that mutate g_GLESCapabilities cannot leak state into later + // tests even if an assertion or exception unwinds the test body early. + struct ScopedGLESCapabilitiesOverride { + ScopedGLESCapabilitiesOverride(): + m_snapshot(MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities) {} + ~ScopedGLESCapabilitiesOverride() { + MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities = m_snapshot; + } + ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete; + ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete; + + private: + MobileGL::MG_External::GLESCapabilities m_snapshot; + }; } // namespace TEST(Sanity, BasicAssertions) { @@ -158,6 +183,74 @@ TEST(DirectGLESSanity, LeavesBaseInstanceBuiltinAloneOutsideVertexShaders) { EXPECT_EQ(rewritten, source); } +TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) { + const ScopedGLESCapabilitiesOverride capsGuard; + auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; + caps.IndirectDrawInstanceIdIncludesBaseInstance = true; + // Deliberately not the GLESCapabilities default (8): the injected block must land at + // MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the + // probed cap and falls back to the struct default would surface as "binding = 7". + caps.MaxShaderStorageBufferBindings = 13; + + const MobileGL::String source = R"(#version 310 es +highp int mg_BaseInstanceLowered; +void main() { + int instance = gl_InstanceID + mg_BaseInstanceLowered; + gl_Position = vec4(float(instance)); +} +)"; + + const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms( + source, GL_VERTEX_SHADER); + + 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))"), + MobileGL::String::npos); + EXPECT_NE(rewritten.find( + "layout(std430, binding = 12) readonly buffer mg_IndirectParams { highp uint mg_indirectWords[]; };"), + MobileGL::String::npos); + // The one inside the #define machinery must be the only surviving gl_InstanceID. + EXPECT_EQ(CountOccurrences(rewritten, "gl_InstanceID"), 1u); +} + +TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) { + 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() { + int instance = gl_InstanceID + mg_BaseInstanceLowered; + gl_Position = vec4(float(instance)); +} +)"; + + const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms( + source, GL_VERTEX_SHADER); + + EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos); + EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos); +} + +TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) { + const ScopedGLESCapabilitiesOverride capsGuard; + auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; + caps.IndirectDrawInstanceIdIncludesBaseInstance = true; + caps.MaxShaderStorageBufferBindings = 13; + + const MobileGL::String source = + "#version 310 es\nhighp int mg_BaseInstanceLowered;\nint value = gl_InstanceID + mg_BaseInstanceLowered;\n"; + + const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms( + source, GL_FRAGMENT_SHADER); + + EXPECT_EQ(rewritten, source); +} + TEST(DirectVulkanSanity, AdvertisesTextureStorageForDirectStateAccess) { MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 57bcf9ee..b7137724 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -549,8 +549,8 @@ namespace MobileGL::MG_Util::BackendLoader { // vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V InstanceIndex, which includes // firstInstance. The DirectGLES native indirect-draw path uses this answer to keep // gl_InstanceID zero-based in rewritten shaders (PromoteDrawParameterGlobalsToUniforms). - static Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps, - const MG_External::GLESFunctionsTable& f) { + Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& f) { const Bool esVersionOk = caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1); if (!esVersionOk || !f.glDrawArraysIndirect || !f.glBindBufferBase || !f.glMapBufferRange || diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index dd75a080..cf3e23f4 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1082,6 +1082,11 @@ namespace MobileGL { void AcquireEGLFunctions(MG_External::EGLFunctionsTable& funcs); Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs); + // Detects whether indirect draws leak the command's baseInstance word into + // gl_InstanceID (see Loader.cpp). Called by FillInGLESCapabilities; exposed + // so MG_Test can drive it against a fake GLES functions table. + Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& glesFuncs); } // namespace MG_Util::BackendLoader } // namespace MobileGL #undef MOBILEGL_EXTERNAL_GLES