[Test, Bench] (ShaderTranspiler): pin L1 backend-agnosticism and measure the whole-front-end hit

This commit is contained in:
Swung0x48
2026-08-20 12:00:01 -04:00
parent 1eeeb44d94
commit cdba7bed2e
2 changed files with 186 additions and 9 deletions
@@ -37,6 +37,7 @@
#include "Init.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
@@ -220,16 +221,91 @@ static void BM_ProgramLink_CacheOn(benchmark::State& state) {
const String vs = kVertexSource;
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
const TranslationCacheStats before = GetSpirvTranslationCache().Stats();
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
for (auto _ : state) {
LinkOneProgram(vs, fs);
}
const TranslationCacheStats stats = GetSpirvTranslationCache().Stats();
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
}
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
//
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
// leaves is the parse, not the link.
//
// This pair keeps the shader objects alive, so ShaderCompileAdoptionMap can hand a later
// glCompileShader the earlier one's parse, and the L1 hit then skips the link, mapIO, the
// SPIR-V, the reflection and the routing outright. That is the
// KHR-GL33.texture_swizzle.smoke_* shape - 2592 programs over a handful of distinct sources.
// ---------------------------------------------------------------------------------------
namespace {
struct SharedShaders {
GLuint vs = 0;
GLuint fs = 0;
};
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
using namespace MG_Impl::GLImpl;
SharedShaders shaders;
shaders.vs = CreateShader(GL_VERTEX_SHADER);
const char* vsText = vertexSource.c_str();
ShaderSource(shaders.vs, 1, &vsText, nullptr);
CompileShader(shaders.vs);
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
const char* fsText = fragmentSource.c_str();
ShaderSource(shaders.fs, 1, &fsText, nullptr);
CompileShader(shaders.fs);
return shaders;
}
void LinkFromSharedShaders(const SharedShaders& shaders) {
using namespace MG_Impl::GLImpl;
const GLuint program = CreateProgram();
AttachShader(program, shaders.vs);
AttachShader(program, shaders.fs);
LinkProgram(program);
benchmark::DoNotOptimize(program);
DeleteProgram(program);
}
} // namespace
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
}
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
// ---------------------------------------------------------------------------------------
@@ -33,6 +33,7 @@
#include "Init.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
#include "MG_Util/ShaderTranspiler/CompileEnv.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
@@ -125,6 +126,8 @@ void main() {
inputs.stages = stages;
inputs.shaderCompileFlags = 0;
inputs.enableSpirvValidation = false;
inputs.xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
inputs.maxFragmentOutputColorNumber = 8;
return inputs;
}
@@ -157,6 +160,42 @@ void main() {
return digest;
}
// Everything the GL query surface says about a linked program, as one string. Used to
// assert that a program served from the L1 memo - which never built a TProgram - answers
// identically to the one that was parsed.
String ReflectionFingerprint(const GLuint program) {
const auto& object = MG_State::pGLContext->GetProgramObject(program);
if (!object) return String();
String out;
const Uint uniformCount = object->GetUniformCount();
for (Uint i = 0; i < uniformCount; ++i) {
out += object->GetActiveUniformName(i);
out += ':' + std::to_string(object->GetActiveUniformType(i));
out += ':' + std::to_string(object->GetActiveUniformArraySize(i));
out += ':' + std::to_string(object->GetActiveUniformBlockIndex(i));
out += ':' + std::to_string(object->GetActiveUniformOffset(i));
out += ':' + std::to_string(object->GetActiveUniformArrayStride(i));
out += ':' + std::to_string(object->GetActiveUniformMatrixStride(i));
out += ':' + std::to_string(object->GetActiveUniformIsRowMajor(i));
out += '\n';
}
const Int attribCount = object->GetActiveAttributesCount();
for (Int i = 0; i < attribCount; ++i) {
out += object->GetActiveAttribName(static_cast<Uint>(i));
out += ':' + std::to_string(object->GetActiveAttribType(static_cast<Uint>(i)));
out += ':' + std::to_string(object->GetActiveAttribArraySize(static_cast<Uint>(i)));
out += '\n';
}
const Int outputCount = object->GetActiveFragmentOutputCount();
for (Int i = 0; i < outputCount; ++i) {
out += object->GetActiveFragmentOutputName(static_cast<Uint>(i));
out += ':' + std::to_string(object->GetFragmentOutputLocation(static_cast<Uint>(i)));
out += ':' + std::to_string(object->GetFragmentOutputType(static_cast<Uint>(i)));
out += '\n';
}
return out;
}
GLuint MakeShader(const GLenum type, const String& source) {
const GLuint shader = MG_Impl::GLImpl::CreateShader(type);
const char* text = source.c_str();
@@ -383,6 +422,8 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
const UnorderedMap<String, Uint> fragOut{{"fragColor", 1u}};
const UnorderedMap<String, Uint> fragIndex{{"fragColor", 1u}};
const UnorderedMap<String, Uint> opaque{{"uTex", 5u}};
const Vector<String> xfbVaryings{"vPos", "gl_NextBuffer", "vUv"};
const Vector<String> xfbVaryingsReordered{"vUv", "gl_NextBuffer", "vPos"};
const SpirvTranslationKeyInputs base = BaselineSpirvInputs(baseStages);
const TranslationCacheKey baseKey = BuildSpirvTranslationKey(base);
@@ -450,6 +491,30 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
v.enableSpirvValidation = true;
variants.emplace_back("enableSpirvValidation", BuildSpirvTranslationKey(v));
}
// ---- inputs the WIDENED payload pulled into the key ----
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
// payload now carries, so they have to split the key. This is the group that would go
// stale first if the payload ever grew again without the key following it.
{ // glTransformFeedbackVaryings: shapes xfbVaryings / xfbStrides / gsStripTriangles
SpirvTranslationKeyInputs v = base;
v.requestedXfbVaryings = &xfbVaryings;
variants.emplace_back("requestedXfbVaryings", BuildSpirvTranslationKey(v));
}
{ // ... and its ORDER, which gl_NextBuffer / gl_SkipComponentsN make load-bearing
SpirvTranslationKeyInputs v = base;
v.requestedXfbVaryings = &xfbVaryingsReordered;
variants.emplace_back("requestedXfbVaryings order", BuildSpirvTranslationKey(v));
}
{ // GL_INTERLEAVED_ATTRIBS vs GL_SEPARATE_ATTRIBS
SpirvTranslationKeyInputs v = base;
v.xfbBufferMode = GL_SEPARATE_ATTRIBS;
variants.emplace_back("xfbBufferMode", BuildSpirvTranslationKey(v));
}
{ // GL_MAX_DRAW_BUFFERS: decides whether the link is REJECTED at all
SpirvTranslationKeyInputs v = base;
v.maxFragmentOutputColorNumber = 4;
variants.emplace_back("maxFragmentOutputColorNumber", BuildSpirvTranslationKey(v));
}
for (const auto& [name, key] : variants) {
EXPECT_FALSE(key == baseKey) << "moving " << name << " did not move the L1 key";
@@ -578,12 +643,12 @@ TEST_F(TranslationCacheTest, L1MemoizesASecondProgramWithIdenticalSources) {
const CacheModeScope cacheOn(true);
const String fs = SwizzleLikeFragment("");
const TranslationCacheStats before = GetSpirvTranslationCache().Stats();
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const GLuint first = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterFirst = GetSpirvTranslationCache().Stats();
const TranslationCacheStats afterFirst = MG_State::GLState::GetProgramTranslationCache().Stats();
const GLuint second = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterSecond = GetSpirvTranslationCache().Stats();
const TranslationCacheStats afterSecond = MG_State::GLState::GetProgramTranslationCache().Stats();
GLint firstStatus = GL_FALSE;
GLint secondStatus = GL_FALSE;
@@ -601,6 +666,42 @@ TEST_F(TranslationCacheTest, L1MemoizesASecondProgramWithIdenticalSources) {
const Vector<Uint64> secondDigest = ProgramSpirvDigest(second);
ASSERT_EQ(firstDigest.size(), 2u);
EXPECT_EQ(firstDigest, secondDigest);
// The payload is the WHOLE front end, so the reflection has to survive it too - the
// program served from the memo never built a TProgram to answer these from.
EXPECT_EQ(ReflectionFingerprint(first), ReflectionFingerprint(second));
EXPECT_FALSE(ReflectionFingerprint(second).empty());
}
// A hit publishes a program that never had a glslang::TProgram at all. Everything GL can ask
// about it has to come out of the payload; if any accessor still needed the parse this would
// answer differently from the program that was parsed.
TEST_F(TranslationCacheTest, AProgramServedFromTheMemoAnswersTheWholeQuerySurface) {
const SyncCompileScope sync;
const CacheModeScope cacheOn(true);
const String fs = SwizzleLikeFragment("");
const GLuint parsed = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterFirst = MG_State::GLState::GetProgramTranslationCache().Stats();
const GLuint fromMemo = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterSecond = MG_State::GLState::GetProgramTranslationCache().Stats();
ASSERT_EQ(afterSecond.hits - afterFirst.hits, 1u) << "the second link was not a hit";
const auto& parsedObject = MG_State::pGLContext->GetProgramObject(parsed);
const auto& memoObject = MG_State::pGLContext->GetProgramObject(fromMemo);
ASSERT_NE(parsedObject, nullptr);
ASSERT_NE(memoObject, nullptr);
EXPECT_EQ(ReflectionFingerprint(parsed), ReflectionFingerprint(fromMemo));
EXPECT_EQ(parsedObject->GetUniformCount(), memoObject->GetUniformCount());
EXPECT_EQ(parsedObject->GetActiveAttributesCount(), memoObject->GetActiveAttributesCount());
EXPECT_EQ(parsedObject->GetActiveFragmentOutputCount(), memoObject->GetActiveFragmentOutputCount());
EXPECT_EQ(parsedObject->GetActiveUniformBlocksCount(), memoObject->GetActiveUniformBlocksCount());
// The uniform shadow (phase B's half of the payload) has to arrive as well.
for (Uint location = 0; location <= parsedObject->GetMaxUniformLocation(); ++location) {
EXPECT_EQ(parsedObject->GetUniformOffset(location), memoObject->GetUniformOffset(location))
<< "uniform offset at location " << location;
}
}
// The modules a hit hands out must be the modules a from-scratch translation would have
@@ -629,9 +730,9 @@ TEST_F(TranslationCacheTest, L1DoesNotMemoizeAcrossDifferentSources) {
// Prime with one, then link a different one: a miss, not a hit.
(void)LinkProgramFromSources(kVertexSource, SwizzleLikeFragment(""));
const TranslationCacheStats before = GetSpirvTranslationCache().Stats();
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
(void)LinkProgramFromSources(kVertexSource, SwizzleLikeFragment("i"));
const TranslationCacheStats after = GetSpirvTranslationCache().Stats();
const TranslationCacheStats after = MG_State::GLState::GetProgramTranslationCache().Stats();
EXPECT_EQ(after.hits - before.hits, 0u);
EXPECT_EQ(after.misses - before.misses, 1u);
@@ -649,9 +750,9 @@ TEST_F(TranslationCacheTest, TheEscapeHatchDisablesL1Entirely) {
}
const CacheModeScope cacheOff(false);
const TranslationCacheStats before = GetSpirvTranslationCache().Stats();
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const GLuint program = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats after = GetSpirvTranslationCache().Stats();
const TranslationCacheStats after = MG_State::GLState::GetProgramTranslationCache().Stats();
GLint status = GL_FALSE;
MG_Impl::GLImpl::GetProgramiv(program, GL_LINK_STATUS, &status);