mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18:30 +09:00
[Test] (MG_Test): cover the indirect gl_InstanceID probe and shader rewrite
BackendLoaderTest drives ProbeIndirectInstanceIdIncludesBaseInstance (now externally linked) against a fake GLES function table: conforming and ANGLE-style leaking drivers, the no-vertex-SSBO skip, draw-error inconclusiveness, object cleanup, and the FillInGLESCapabilities wiring end-to-end. SanityTest gains PromoteDrawParameterGlobalsToUniforms cases pinning the mg_ZeroBasedInstanceID rewrite and the last-SSBO-binding computation against a non-default binding count, with RAII capability restoration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
// 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<GLuint, std::vector<unsigned char>> bufferStores; // buffer id -> data store
|
||||
std::map<GLenum, GLuint> boundBuffers; // target -> buffer id
|
||||
std::map<GLuint, GLuint> 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<unsigned char>* 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<const GLubyte*>("MobileGL Fake Vendor");
|
||||
case GL_RENDERER:
|
||||
return reinterpret_cast<const GLubyte*>("MobileGL Fake Renderer");
|
||||
case GL_VERSION:
|
||||
return reinterpret_cast<const GLubyte*>("OpenGL ES 3.1 (MobileGL fake)");
|
||||
case GL_SHADING_LANGUAGE_VERSION:
|
||||
return reinterpret_cast<const GLubyte*>("OpenGL ES GLSL ES 3.10 (MobileGL fake)");
|
||||
default:
|
||||
return reinterpret_cast<const GLubyte*>("");
|
||||
}
|
||||
};
|
||||
// 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();
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user