diff --git a/MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp b/MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp index b2620964..61b8b9a8 100644 --- a/MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp +++ b/MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp @@ -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(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(stats.hits - before.hits); state.counters["L1_misses"] = static_cast(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(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(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(stats.hits - before.hits); + state.counters["L1_misses"] = static_cast(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. // --------------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index 37a31ac4..c5eba489 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -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(i)); + out += ':' + std::to_string(object->GetActiveAttribType(static_cast(i))); + out += ':' + std::to_string(object->GetActiveAttribArraySize(static_cast(i))); + out += '\n'; + } + const Int outputCount = object->GetActiveFragmentOutputCount(); + for (Int i = 0; i < outputCount; ++i) { + out += object->GetActiveFragmentOutputName(static_cast(i)); + out += ':' + std::to_string(object->GetFragmentOutputLocation(static_cast(i))); + out += ':' + std::to_string(object->GetFragmentOutputType(static_cast(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 fragOut{{"fragColor", 1u}}; const UnorderedMap fragIndex{{"fragColor", 1u}}; const UnorderedMap opaque{{"uTex", 5u}}; + const Vector xfbVaryings{"vPos", "gl_NextBuffer", "vUv"}; + const Vector 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 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);