[Perf] (ProgramState): serve a whole linked program from translation cache L1, skipping the link entirely

This commit is contained in:
Swung0x48
2026-08-20 12:00:01 -04:00
parent 14744f117c
commit 1eeeb44d94
9 changed files with 348 additions and 125 deletions
+1
View File
@@ -389,6 +389,7 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
+3
View File
@@ -18,6 +18,7 @@
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <atomic>
@@ -79,6 +80,8 @@ namespace MobileGL {
// fordebug build gets one line per level saying how the run went.
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
MG_State::GLState::LogProgramTranslationCacheStats();
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
if (logLifecycle) {
@@ -8,6 +8,8 @@
#include "ProgramLinkTask.h"
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -373,6 +375,20 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Link body start, shaders to link: %zu", in.externalIndex, in.shaders.size());
if (!ValidateAttachedShaders()) return;
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
// run before the L1 probe, which needs the merged opaque bindings in its key.
MergeShaderSideChannels();
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
// ---- L1 of the shader translation memo ----
// Everything below this point - the parse, the link, mapIO, GlslangToSpv, spirv-opt,
// buildReflection and the global-UBO routing - is what a hit skips. See
// ProgramTranslationCache.h.
spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env);
if (TryPublishFromTranslationCache()) return;
Vector<SharedPtr<glslang::TShader>> shaders;
if (!ConsumeShaders(shaders)) return;
@@ -399,30 +415,6 @@ namespace MobileGL::MG_State::GLState {
}
}
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
for (const auto& [name, location] : compiled.explicitUniformLocations) {
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
if (!inserted && it->second != location) {
artifacts.infoLog = std::format(
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
"across stages.",
name, it->second, location);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
artifacts.explicitOpaqueUniformBindings[name] = binding;
}
}
ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation,
@@ -570,7 +562,14 @@ namespace MobileGL::MG_State::GLState {
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env);
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
if (spirvHandoff.spirvCacheKey.Valid()) {
auto forCache = MakeShared<ProgramObject::LinkArtifacts>(artifacts);
forCache->program.reset();
spirvHandoff.linkArtifactsForCache = Move(forCache);
}
spirvHandoff.ready = true;
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
spirvHandoff.shaderTypes.size());
@@ -579,12 +578,9 @@ namespace MobileGL::MG_State::GLState {
// The L1 key. Every input below is one that can change the SPIR-V this program
// generates; see the key inventory on SpirvTranslationKeyInputs.
//
// Deliberately NOT keyed on: the transform-feedback request
// (ResolveTransformFeedbackVaryings only READS the linked intermediates - it sets no
// XFB qualifier, and the ESSL capture rename happens in the backend, behind L2's own
// key), the fragment-output count limit (a link-failure gate, never an emission input),
// and reflection (verified non-mutating on this glslang pin; see the ordering note in
// RunBody).
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
// classification on CompileEnv::frontendFingerprint, and L2's own key in
// MG_Util/ShaderTranspiler/TranslationCache.h.
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
using namespace MG_Util::ShaderTranspiler;
@@ -617,12 +613,78 @@ namespace MobileGL::MG_State::GLState {
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
// In the key ONLY because the payload now carries the reflection: transform feedback
// is resolved by reading the linked intermediates and never perturbs the generated
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
// gsStripTriangles, and maxFragmentOutputColorNumber decides whether the link is
// rejected at all. Widening a payload means widening the key.
keyInputs.requestedXfbVaryings = &in.requestedXfbVaryings;
keyInputs.xfbBufferMode = static_cast<Uint32>(in.requestedXfbBufferMode);
keyInputs.maxFragmentOutputColorNumber = in.maxFragmentOutputColorNumber;
return BuildSpirvTranslationKey(keyInputs);
}
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
outShaders.assign(in.shaders.size(), nullptr);
// The link rejections that need nothing but the compile snapshots. They run before the
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
// The two lexical side channels the relaxed parse cannot provide, merged across stages:
// explicit default-block uniform locations (which must agree, or the link fails) and
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
// of the L1 memo key.
void ProgramLinkTask::MergeShaderSideChannels() {
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
for (const auto& [name, location] : compiled.explicitUniformLocations) {
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
if (!inserted && it->second != location) {
artifacts.infoLog = std::format(
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
"across stages.",
name, it->second, location);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
artifacts.explicitOpaqueUniformBindings[name] = binding;
}
}
}
// An L1 hit: the entire front end, published without constructing a TShader or a
// TProgram. Everything here is a copy out of plain owned data - `link.program` is null in
// the payload by construction, and nothing reads it any more.
Bool ProgramLinkTask::TryPublishFromTranslationCache() {
if (!spirvHandoff.spirvCacheKey.Valid()) return false;
const ProgramTranslationResultPtr hit =
GetProgramTranslationCache().Find(spirvHandoff.spirvCacheKey);
if (!hit) return false;
artifacts = hit->link;
spirvHandoff.shaderTypes.resize(in.shaders.size());
for (SizeT i = 0; i < in.shaders.size(); i++) {
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
}
// An ALIASING SharedPtr: it points at the payload's SpirvArtifacts while sharing
// ownership of the whole payload, so phase B publishes them without a second copy and
// without any chance of the entry being evicted from under it.
spirvHandoff.cachedSpirv =
SharedPtr<const ProgramObject::SpirvArtifacts>(hit, &hit->spirv);
spirvHandoff.ready = true;
MGLOG_D("ProgramObject %u: L1 cache hit - the whole front end was reused; no parse, no "
"link, no SPIR-V generation",
in.externalIndex);
return true;
}
Bool ProgramLinkTask::ValidateAttachedShaders() {
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
// the compute pipeline has no other stages to link against, so a program that mixes
// them must fail to link (KHR-GL43.compute_shader.api-program).
@@ -644,8 +706,6 @@ namespace MobileGL::MG_State::GLState {
const LinkShaderInput& input = in.shaders[i];
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
const ShaderCompileArtifacts& compiled = CompiledArtifacts(input.compiled);
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
MG_Util::ConvertGLEnumToString(shaderType).c_str());
if (!compiled.compileStatus) {
// The compile log LEADS the quoted source, and that order is load-bearing:
@@ -664,6 +724,17 @@ namespace MobileGL::MG_State::GLState {
in.externalIndex, i, artifacts.infoLog));
return false;
}
}
return true;
}
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
outShaders.assign(in.shaders.size(), nullptr);
for (SizeT i = 0; i < in.shaders.size(); i++) {
const LinkShaderInput& input = in.shaders[i];
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
MG_Util::ConvertGLEnumToString(shaderType).c_str());
String reparseLog;
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
if (!outShaders[i]) {
@@ -130,6 +130,14 @@ namespace MobileGL::MG_State::GLState {
// without preprocessed source - in which case phase B simply translates.
MG_Util::ShaderTranspiler::TranslationCacheKey spirvCacheKey;
// Set on an L1 HIT: phase B publishes these SpirvArtifacts verbatim instead of
// generating anything. Null on a miss.
SharedPtr<const ProgramObject::SpirvArtifacts> cachedSpirv;
// Set on a MISS: the LinkArtifacts phase B has to pair with its own SpirvArtifacts
// to insert the completed front end. Copied here rather than read off the node,
// because the GL-thread join MOVES `artifacts` out before phase B runs.
SharedPtr<const ProgramObject::LinkArtifacts> linkArtifactsForCache;
// The one flag phase B tests before doing anything: false means this link never
// reached the tail of RunBody (it failed, or was cancelled mid-body).
Bool ready = false;
@@ -156,7 +164,20 @@ namespace MobileGL::MG_State::GLState {
// ---- the link body, split exactly as ProgramObject::Link() had it ----
// Each returns false to abort the link with `artifacts.infoLog` already set, which is
// GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error.
// The two link-rejection gates that need no parsed shader: a compute stage mixed
// with any other, and an attached shader that failed to compile. Split out of
// ConsumeShaders so they still run - in the same order, with the same diagnostics -
// BEFORE the L1 memo is consulted, rather than behind a hit that would skip them.
// The two lexical side channels the relaxed parse cannot provide, merged across
// stages. Reads the compile snapshots only, so it runs before any parse - the merged
// opaque bindings are part of the L1 memo key. Sets artifacts.infoLog and leaves
// linkStatus false when two stages disagree on an explicit uniform location.
void MergeShaderSideChannels();
Bool ValidateAttachedShaders();
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
// Publishes a whole front end straight out of the L1 memo: no TShader, no TProgram,
// no SPIR-V generation. Returns false on a miss.
Bool TryPublishFromTranslationCache();
// The L1 memo key for the SPIR-V this program is about to generate, or an invalid
// key when the cache is off or a stage has no preprocessed source to key on.
@@ -12,6 +12,7 @@
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/ShaderTranspiler/Types.h>
@@ -96,11 +97,25 @@ namespace MobileGL::MG_State::GLState {
// and `diagnostics`, and this node is the sole reader of the handoff.
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
const Uint externalIndex = m_phaseA->in.externalIndex;
if (!handoff.ready || !handoff.reflection.program) {
if (!handoff.ready) {
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
// Publish nothing; spirvStatus stays false.
return;
}
// A TProgram is required only to GENERATE. A link served from the L1 memo has none by
// construction - that is the entire point of the widened payload - and its SPIR-V and
// routing tables arrive ready-made in cachedSpirv.
if (!handoff.cachedSpirv && !handoff.reflection.program) return;
// An L1 hit already carries everything this phase would have produced. Publish it
// and stop: no GlslangToSpv, no spirv-opt, no routing pass.
if (handoff.cachedSpirv) {
artifacts = *handoff.cachedSpirv;
MGLOG_D("ProgramObject %u: L1 cache hit - %zu SPIR-V module(s) and the global-UBO "
"routing reused",
externalIndex, artifacts.generatedSpirv.size());
return;
}
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
const Bool deferOutputValidationForDirectVulkan =
@@ -138,6 +153,23 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
BuildGlobalUboRouting(handoff, externalIndex);
// The completed front end goes into the L1 memo HERE, where both halves exist: phase
// A's LinkArtifacts (carried in the handoff) and this phase's SpirvArtifacts.
//
// Only a clean run is memoized. A failed optimizer run leaves a module as whatever the
// chain got to before it gave up, and that is exactly the binary no other program
// should ever be handed.
if (artifacts.spirvStatus && handoff.spirvCacheKey.Valid() && handoff.linkArtifactsForCache) {
auto payload = MakeShared<ProgramTranslationResult>();
payload->link = *handoff.linkArtifactsForCache;
payload->link.program.reset(); // belt and braces: never memoize a glslang arena
payload->spirv = artifacts;
const SizeT payloadBytes = ProgramTranslationResultBytes(*payload);
GetProgramTranslationCache().Insert(handoff.spirvCacheKey,
ProgramTranslationResultPtr(Move(payload)),
payloadBytes);
}
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
artifacts.generatedSpirv.size());
}
@@ -153,28 +185,6 @@ namespace MobileGL::MG_State::GLState {
using namespace MG_Util::ShaderTranspiler;
MGLOG_D("ProgramObject %u: GenerateSpirv - start", externalIndex);
// L1 of the shader translation memo. The segment this short-circuits is the whole
// of GlslangToSpv plus the 11-pass SanitizeAndOptimizeBinary chain, for every stage
// of the program at once - ~136 us per stage on the RelWithDebInfo host measurement.
// The key was built at the tail of phase A (ProgramLinkTask::BuildSpirvCacheKey) and
// covers every input that can move these bytes; see TranslationCache.h.
//
// Note what a HIT does NOT skip: the glslang parse and link, which already happened
// in phase A because the frontend's whole GL query surface is built out of the
// TProgram they produce.
auto& spirvCache = GetSpirvTranslationCache();
const TranslationCacheKey& cacheKey = handoff.spirvCacheKey;
if (cacheKey.Valid()) {
if (const SpirvTranslationResultPtr hit = spirvCache.Find(cacheKey);
hit && hit->modules.size() == handoff.shaderTypes.size()) {
artifacts.generatedSpirv = hit->modules;
artifacts.spirvStatus = true;
MGLOG_D("ProgramObject %u: GenerateSpirv - L1 cache hit, %zu module(s) reused",
externalIndex, artifacts.generatedSpirv.size());
return;
}
}
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
// configuration, and the handoff's program linked those parses - so it IS the program
// the backends consume. Generate SPIR-V straight from its intermediates, which the
@@ -218,16 +228,6 @@ namespace MobileGL::MG_State::GLState {
}
}
artifacts.spirvStatus = allOptimized;
// Only a clean run is memoized. A failed optimizer run leaves `spv` as whatever the
// chain got to before it gave up, and that is exactly the binary no other program
// should ever be handed.
if (allOptimized && cacheKey.Valid()) {
auto payload = MakeShared<SpirvTranslationResult>();
payload->modules = artifacts.generatedSpirv;
const SizeT payloadBytes = SpirvTranslationResultBytes(*payload);
spirvCache.Insert(cacheKey, SpirvTranslationResultPtr(Move(payload)), payloadBytes);
}
}
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
@@ -0,0 +1,82 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.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 "ProgramTranslationCache.h"
namespace MobileGL::MG_State::GLState {
namespace {
// ---- L1 caps: 48 entries / 24 MiB ----
//
// Both numbers moved when the payload grew from "the SPIR-V modules" to "the whole
// front end". An entry is now the stages' preprocessed source (the key), the SPIR-V,
// the reflection snapshot and the global-UBO shadow - roughly twice what it was - so
// the byte budget doubled and the entry count came down to keep the worst case in the
// same place on a phone.
//
// The shape of the choice has not changed: this cache exists for REPETITION, not
// coverage. A KHR-GL33.texture_swizzle smoke case builds 2592 programs out of fewer
// than ten distinct ones, so a handful of entries serves it completely; an Iris
// shaderpack load is ~300-600 MOSTLY DISTINCT programs that would never hit however
// large the cache is, so a bigger cap there buys nothing and costs resident memory.
// 48 is comfortably above the distinct-program count of every repetition workload
// measured, and 24 MiB bounds the pathological case - a pack whose ~100 KB stages
// really are re-linked - at roughly three times the existing 8 MiB
// ShaderPreprocessCache budget, which is the other memo on this path.
constexpr SizeT kMaxEntries = 48;
constexpr SizeT kMaxBytes = 24u * 1024u * 1024u;
SizeT StringsBytes(const Vector<String>& values) {
SizeT bytes = 0;
for (const String& value : values) bytes += value.size() + sizeof(String);
return bytes;
}
SizeT ResourcesBytes(const Vector<ProgramObject::ResourceReflection>& records) {
SizeT bytes = records.size() * sizeof(ProgramObject::ResourceReflection);
for (const auto& record : records) bytes += record.name.size();
return bytes;
}
} // namespace
// Approximate on purpose: it feeds a budget, not an allocator. It counts the things that
// actually scale with shader size - the SPIR-V, the reflection names, the UBO shadow -
// and ignores per-entry fixed overhead.
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result) {
SizeT bytes = 0;
for (const auto& module : result.spirv.generatedSpirv) bytes += module.size() * sizeof(unsigned);
bytes += result.spirv.uniformOffsets.size() * sizeof(Uint);
bytes += result.spirv.globalUboScratch.size();
bytes += ResourcesBytes(result.link.uniformReflection);
bytes += ResourcesBytes(result.link.blockReflection);
bytes += ResourcesBytes(result.link.pipeInputReflection);
bytes += ResourcesBytes(result.link.pipeOutputReflection);
bytes += StringsBytes(result.link.attribs);
bytes += StringsBytes(result.link.xfbInterfaceNames);
bytes += result.link.infoLog.size();
return bytes;
}
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
GetProgramTranslationCache() {
// DELIBERATELY LEAKED - see the same note on the L2 cache in
// MG_Util/ShaderTranspiler/TranslationCache.cpp. A function-local static OBJECT
// registers its destructor at first use, and first use here is a ShaderCompilePool
// worker; ShaderCompilePool's own atexit drain sentinel is registered strictly
// earlier, and exit handlers run in reverse order - so the cache would be destroyed
// while workers were still inserting into it. A function-local static POINTER is
// trivially destructible and registers no exit handler at all.
static auto* const kCache =
new MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>(
"ShaderTranslationCache L1 (GLSL->front end)", kMaxEntries, kMaxBytes);
return *kCache;
}
void ClearProgramTranslationCache() { GetProgramTranslationCache().Clear(); }
void LogProgramTranslationCacheStats() { GetProgramTranslationCache().LogStats(); }
} // namespace MobileGL::MG_State::GLState
@@ -0,0 +1,61 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.h
// 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
#pragma once
#include <Includes.h>
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
namespace MobileGL::MG_State::GLState {
// ===================================================================================
// L1 of the shader translation memo: THE WHOLE FRONT END of one glLinkProgram.
//
// A hit skips the glslang parse, the glslang link and mapIO, GlslangToSpv, the 11-pass
// SanitizeAndOptimizeBinary chain, buildReflection, and the global-UBO routing pass. No
// TShader and no TProgram is constructed at all - which is only possible because the GL
// query surface no longer reads one (see ProgramObject::UniformReflection and
// ProgramLinkTask::SnapshotGlslangReflection).
//
// WHY THE PAYLOAD IS THE WHOLE THING rather than just the SPIR-V: the frontend answers
// glGetActiveUniform, glGetProgramResource*, glGetUniformLocation and the rest out of
// LinkArtifacts, and glUniform*/glGetUniform* out of SpirvArtifacts. Caching only the
// modules would have left the parse and the link on the hot path to rebuild exactly the
// data the payload can carry - and the parse alone is ~48% of a CTS-shaped program build.
//
// WHY IT LIVES HERE AND NOT IN MG_Util: the payload is a ProgramObject::LinkArtifacts
// plus a ProgramObject::SpirvArtifacts, and MG_Util must not depend on MG_State. The
// KEY is plain bytes and stays in MG_Util (BuildSpirvTranslationKey), so both layers
// agree on exactly one definition of "the same front-end input".
//
// EVERYTHING IN THE PAYLOAD IS PLAIN OWNED DATA. `link.program` is null by construction:
// the whole point is that a hit never has a glslang arena to point into. Both structs
// were audited field by field - the only member that ever pointed into glslang-owned
// memory was `program` itself, and TUniformInitializer / XfbVarying, which look like
// glslang types, are std::string + std::vector aggregates.
struct ProgramTranslationResult {
// program == nullptr, always. Asserted at insert.
ProgramObject::LinkArtifacts link;
ProgramObject::SpirvArtifacts spirv;
};
using ProgramTranslationResultPtr = SharedPtr<const ProgramTranslationResult>;
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result);
// Process-global, and safe to be: the FRONT-END environment fingerprint is in the key
// (see CompileEnv::frontendFingerprint), so a program built under one context's glslang
// limits can never be handed to a context with different ones - while two contexts on
// DIFFERENT GPUs that agree on those limits deliberately share entries.
//
// Global rather than per-context because the producer runs on a ShaderCompilePool worker
// and must not reach MG_State::pGLContext.
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
GetProgramTranslationCache();
void ClearProgramTranslationCache();
void LogProgramTranslationCacheStats();
} // namespace MobileGL::MG_State::GLState
@@ -34,22 +34,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(MG_Config::CacheVersion);
}
// ---- L1 caps -------------------------------------------------------
// 64 entries / 12 MiB.
//
// The win this cache exists for is REPETITION, not coverage: a CTS smoke
// case compiles a handful of distinct sources 2592 times, and a handful of
// entries serves it completely. The opposite workload - an Iris shaderpack
// load - is ~300-600 MOSTLY DISTINCT programs, which would never hit no
// matter how large the cache is, so a large cap there buys nothing and
// costs resident memory on a phone. 64 entries is comfortably above the
// distinct-source count of every repetition workload measured, and the
// 12 MiB ceiling bounds the pathological case (a pack whose ~100 KB stages
// ARE re-linked) at the same order as the existing 8 MiB
// ShaderPreprocessCache budget.
constexpr SizeT kSpirvCacheMaxEntries = 64;
constexpr SizeT kSpirvCacheMaxBytes = 12u * 1024u * 1024u;
// ---- L2 caps -------------------------------------------------------
// 128 entries / 12 MiB. Same reasoning, twice the entry count: L2 is keyed
// per STAGE rather than per program, so the same program population needs
@@ -84,6 +68,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
Bytes(words.data(), words.size() * sizeof(Uint32));
}
void TranslationKeyBuilder::TextList(const Vector<String>& values) {
Value(static_cast<Uint64>(values.size()));
for (const String& value : values) Text(value);
}
void TranslationKeyBuilder::NameSet(const std::set<String>& names) {
Value(static_cast<Uint64>(names.size()));
for (const String& name : names) Text(name);
@@ -112,15 +101,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
builder.NameMap(inputs.explicitOpaqueUniformBindings ? *inputs.explicitOpaqueUniformBindings : kEmpty);
static const Vector<String> kNoXfb;
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
builder.Value(inputs.xfbBufferMode);
builder.Value(inputs.maxFragmentOutputColorNumber);
return MakeTranslationCacheKey(builder);
}
SizeT SpirvTranslationResultBytes(const SpirvTranslationResult& result) {
SizeT bytes = 0;
for (const auto& module : result.modules) bytes += module.size() * sizeof(Uint32);
return bytes;
}
TranslationCacheKey BuildEsslTranslationKey(const EsslTranslationKeyInputs& inputs) {
TranslationKeyBuilder builder;
AppendCommonKeyPrefix(builder, kEsslKeyTag);
@@ -172,25 +159,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// A function-local static POINTER is trivially destructible, so no exit handler is
// registered for it at all. ClearShaderTranslationCaches() is what releases the memory
// at a controlled point (eglTerminate), after the pool has been drained.
BoundedTranslationCache<SpirvTranslationResult>& GetSpirvTranslationCache() {
static auto* const kCache = new BoundedTranslationCache<SpirvTranslationResult>(
"ShaderTranslationCache L1 (GLSL->SPIR-V)", kSpirvCacheMaxEntries, kSpirvCacheMaxBytes);
return *kCache;
}
BoundedTranslationCache<EsslTranslationResult>& GetEsslTranslationCache() {
static auto* const kCache = new BoundedTranslationCache<EsslTranslationResult>(
"ShaderTranslationCache L2 (SPIR-V->ESSL)", kEsslCacheMaxEntries, kEsslCacheMaxBytes);
return *kCache;
}
void ClearShaderTranslationCaches() {
GetSpirvTranslationCache().Clear();
GetEsslTranslationCache().Clear();
}
void ClearShaderTranslationCaches() { GetEsslTranslationCache().Clear(); }
void LogShaderTranslationCacheStats() {
GetSpirvTranslationCache().LogStats();
GetEsslTranslationCache().LogStats();
}
void LogShaderTranslationCacheStats() { GetEsslTranslationCache().LogStats(); }
} // namespace MobileGL::MG_Util::ShaderTranspiler
@@ -34,13 +34,17 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// bits, and folding them into one key would make every DirectGLES capability a
// reason to miss on the frontend half as well.
//
// WHAT IS DELIBERATELY NOT MEMOIZED: the glslang parse and the glslang link.
// Both produce a TShader/TProgram, and the frontend's whole GL query surface
// (ProgramObject::LinkArtifacts, BuildGlobalUboRouting) is built by asking that
// TProgram questions - so skipping them means caching a live glslang object
// graph and sharing it between ProgramObjects, which is a different change with
// its own aliasing and consume-once hazards. See the report in the branch
// history; the parse is ~50% of the per-stage cost and is the next campaign.
// AN L1 HIT SKIPS THE WHOLE FRONT END - the glslang parse, the link and mapIO,
// GlslangToSpv, spirv-opt, buildReflection and the global-UBO routing. No
// TShader and no TProgram is constructed. That is possible because the payload
// is the whole front-end OUTPUT (LinkArtifacts + SpirvArtifacts, both plain
// owned data) rather than the SPIR-V alone, and because the GL query surface no
// longer reads a live TProgram to answer anything - see
// ProgramObject::UniformReflection and ProgramLinkTask::SnapshotGlslangReflection.
// Caching the live glslang object graph instead would have been the other way to
// get here, and was rejected: TObjectReflection::type points into the TProgram's
// own pool allocator, so sharing one between ProgramObjects is an aliasing and
// consume-once hazard.
//
// CORRECTNESS RULE, non-negotiable. A wrong hit is a silently miscompiled
// shader - far worse than a slow one. So:
@@ -109,6 +113,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// std::set is already ordered, but it gets the same length prefix.
void NameSet(const std::set<String>& names);
// ORDER-SENSITIVE, unlike NameMap: a transform-feedback capture list is a sequence,
// and gl_NextBuffer / gl_SkipComponentsN make its order load-bearing.
void TextList(const Vector<String>& values);
const String& Blob() const { return m_blob; }
String Take() { return Move(m_blob); }
@@ -351,13 +358,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// vertex stage's outputs, so a stage's SPIR-V is NOT a function of that
// stage's source alone. A per-stage key here would be exactly the silent
// miscompile this cache must never produce.
struct SpirvTranslationResult {
// One module per stage, in the same order as ProgramLinkTask's
// spirvHandoff.shaderTypes.
Vector<Vector<Uint32>> modules;
};
using SpirvTranslationResultPtr = SharedPtr<const SpirvTranslationResult>;
struct SpirvTranslationKeyInputs {
struct Stage {
GLenum type = 0;
@@ -374,17 +374,26 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
Uint32 shaderCompileFlags = 0;
Bool enableSpirvValidation = false;
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
// When the payload was SPIR-V alone these were provably irrelevant: transform
// feedback is resolved by READING the linked intermediates and never writes an XFB
// qualifier, and the fragment-output limit is a link-failure gate, so neither can
// move a single word of the generated module. Both DO shape LinkArtifacts
// (xfbVaryings / xfbStrides / xfbBufferMode / gsStripTriangles, and whether the link
// is rejected at all), so widening the payload to the whole front end pulled them
// into the key. Widening a payload means widening the key.
const Vector<String>* requestedXfbVaryings = nullptr;
Uint32 xfbBufferMode = 0;
Int32 maxFragmentOutputColorNumber = 0;
};
TranslationCacheKey BuildSpirvTranslationKey(const SpirvTranslationKeyInputs& inputs);
SizeT SpirvTranslationResultBytes(const SpirvTranslationResult& result);
// Process-global, and safe to be: the CompileEnv fingerprint is in the key, so
// a module computed under one context's limits can never be handed to another
// context with different ones. Global rather than per-context because the
// producer (ProgramSpirvTask) runs on a pool worker and must not reach
// MG_State::pGLContext.
BoundedTranslationCache<SpirvTranslationResult>& GetSpirvTranslationCache();
// The L1 PAYLOAD and its cache instance live in
// MG_State/GLState/ProgramState/ProgramTranslationCache.h, not here: the payload is a
// whole ProgramObject::LinkArtifacts + SpirvArtifacts, and MG_Util must not depend on
// MG_State. Only the key - which is plain bytes - is built here, so both layers agree on
// one definition of "the same front-end input".
// =======================================================================
// L2 - the BACK END: sanitized SPIR-V -> DirectGLES ESSL payload.