mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
168 lines
8.4 KiB
C++
168 lines
8.4 KiB
C++
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.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
|
|
//
|
|
// The gate on the gl_BaseInstance indirect lowering in
|
|
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
|
|
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
|
|
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
|
|
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
|
|
// other graphics stages.
|
|
//
|
|
// Emitting the block on such a driver does not make it work. The driver refuses the program at
|
|
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
|
|
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
|
|
// driver's, the application is told the program linked and then every draw with it renders
|
|
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
|
|
// per-command baseInstance of an indirect draw.
|
|
//
|
|
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
#include <MG_Backend/DirectGLES/Managers.h>
|
|
|
|
using MobileGL::Bool;
|
|
using MobileGL::String;
|
|
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
|
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
|
|
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
|
|
|
|
namespace {
|
|
// The capability block is a process-global the backend fills in at init; restore whatever
|
|
// was there so ordering between this suite and any other that touches it cannot matter.
|
|
struct ScopedGLESCapabilitiesOverride {
|
|
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
|
|
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
|
|
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
|
|
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
|
|
|
|
MobileGL::MG_External::GLESCapabilities saved;
|
|
};
|
|
|
|
Bool Contains(const String& haystack, const String& needle) {
|
|
return haystack.find(needle) != String::npos;
|
|
}
|
|
|
|
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
|
|
// gl_BaseInstance to a Private global.
|
|
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
|
|
highp int mg_BaseInstanceLowered;
|
|
void main() {
|
|
int instance = gl_InstanceID + mg_BaseInstanceLowered;
|
|
gl_Position = vec4(float(instance));
|
|
}
|
|
)";
|
|
} // namespace
|
|
|
|
// One block is all the indirect view needs, so the predicate is a >= 1 test.
|
|
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
|
|
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
|
|
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
|
|
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
|
|
}
|
|
|
|
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
|
|
// what produces the unlinkable program. Unusable, not clamped up to one.
|
|
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
|
|
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
|
|
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
|
|
}
|
|
|
|
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
|
|
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
|
|
|
|
const String rewritten =
|
|
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
|
|
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
|
|
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
|
|
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
|
|
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
|
|
<< rewritten;
|
|
}
|
|
|
|
// The bug this gate exists for. The block must not appear at all - not at a different binding,
|
|
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
|
|
// the whole program unlinkable, and the frontend never surfaces that failure.
|
|
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
|
|
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
|
|
const String rewritten =
|
|
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
|
|
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
|
|
EXPECT_FALSE(Contains(rewritten, "buffer"));
|
|
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
|
// Nothing reads the word index any more, so nothing may declare it either - its presence is
|
|
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
|
|
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
|
|
}
|
|
|
|
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
|
|
// what the non-indirect draw entry points already write.
|
|
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
|
|
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
|
|
const String rewritten =
|
|
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
|
|
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
|
|
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
|
|
// The global declaration must be gone; leaving it would shadow the define.
|
|
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
|
|
}
|
|
|
|
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
|
|
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
|
|
// twice from every non-indirect draw, which is worse than not rebasing at all.
|
|
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
|
|
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
|
|
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
|
|
const String rewritten =
|
|
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
|
|
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
|
|
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
|
|
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
|
}
|
|
|
|
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
|
|
// uniforms with no storage block behind them and must still be promoted on such a driver.
|
|
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
|
|
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
|
|
const String source = R"(#version 310 es
|
|
highp int mg_DrawID;
|
|
highp int mg_BaseVertex;
|
|
void main() {
|
|
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
|
|
}
|
|
)";
|
|
|
|
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
|
|
|
|
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
|
|
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
|
|
}
|