mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
219 lines
12 KiB
C++
219 lines
12 KiB
C++
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/TranslationCache.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 "TranslationCache.h"
|
|
|
|
#include <Config.h>
|
|
|
|
namespace MobileGL::MG_Util::ShaderTranspiler {
|
|
namespace {
|
|
// Tags keep two different key builders from ever producing the same blob,
|
|
// even if their inputs happened to serialize identically.
|
|
constexpr Uint32 kSpirvKeyTag = 0x4d474c31u; // "MGL1"
|
|
constexpr Uint32 kEsslKeyTag = 0x4d474c32u; // "MGL2"
|
|
constexpr Uint32 kParseVerdictKeyTag = 0x4d474c43u; // "MGLC" - L1c, the compile half
|
|
|
|
// Bumped whenever the SHAPE of a key changes (a field added, a field's
|
|
// meaning changed). It is in every blob, so a stale in-memory entry from a
|
|
// previous shape cannot be honoured - and a future disk tier gets the same
|
|
// protection for free.
|
|
//
|
|
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
|
// prints it into the emitted ESSL), and L1c was added.
|
|
constexpr Uint32 kKeyLayoutVersion = 2u;
|
|
|
|
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
|
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
|
// cache - one process cannot hold two of them - but it is the knob a disk
|
|
// tier would have to turn, and putting it in now means the blob format does
|
|
// not have to change when that tier arrives.
|
|
void AppendCommonKeyPrefix(TranslationKeyBuilder& builder, const Uint32 tag) {
|
|
builder.Value(tag);
|
|
builder.Value(kKeyLayoutVersion);
|
|
builder.Value(MG_Config::CacheVersion);
|
|
}
|
|
|
|
// ---- 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
|
|
// roughly twice the slots. The byte budget stays put - an L2 entry (SPIR-V
|
|
// in, ESSL text out) is smaller than an L1 one (all stages' source in, all
|
|
// stages' SPIR-V out).
|
|
constexpr SizeT kEsslCacheMaxEntries = 128;
|
|
constexpr SizeT kEsslCacheMaxBytes = 12u * 1024u * 1024u;
|
|
|
|
// ---- L1c caps ------------------------------------------------------
|
|
// 256 entries / 8 MiB. Per-STAGE like L2, so twice L1's entry count again, and
|
|
// deliberately generous on count because an L1c entry's PAYLOAD is two words and a
|
|
// usually-empty string - all of an entry's weight is its key, i.e. the preprocessed
|
|
// source. 8 MiB is exactly ShaderPreprocessCache's budget, and for the same reason:
|
|
// these two store the same kind of thing (one copy of a shader's text) and neither
|
|
// should be the one that decides how much source a process keeps resident.
|
|
//
|
|
// Sized for REPETITION, like the other two. A CTS smoke case has fewer than ten
|
|
// distinct stages and fits many times over; an Iris pack load is hundreds of ~100 KB
|
|
// mostly-distinct stages that would not hit at any cap, so a bigger budget there buys
|
|
// nothing and costs resident memory on a phone.
|
|
constexpr SizeT kParseVerdictCacheMaxEntries = 256;
|
|
constexpr SizeT kParseVerdictCacheMaxBytes = 8u * 1024u * 1024u;
|
|
} // namespace
|
|
|
|
Bool ShaderTranslationCacheEnabled() {
|
|
// Read live rather than latched into a function-local static. MG_Config::Features
|
|
// is a plain global of scalars written once by MG_ConfigLoader::Init() - a load
|
|
// costs nothing, no worker ever touches the environment through it, and the unit
|
|
// tests (which flip the field directly, as AsyncCompileTest and QueryTest already
|
|
// do) need the switch to actually take effect when they flip it.
|
|
return MG_Config::Features.ShaderTranslationCache != MG_Config::QuirkOverride::ForceOff;
|
|
}
|
|
|
|
void TranslationKeyBuilder::Bytes(const void* data, const SizeT length) {
|
|
if (length == 0) return;
|
|
m_blob.append(static_cast<const char*>(data), length);
|
|
}
|
|
|
|
void TranslationKeyBuilder::Text(const StringView text) {
|
|
Value(static_cast<Uint64>(text.size()));
|
|
Bytes(text.data(), text.size());
|
|
}
|
|
|
|
void TranslationKeyBuilder::Words(const Vector<Uint32>& words) {
|
|
Value(static_cast<Uint64>(words.size()));
|
|
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);
|
|
}
|
|
|
|
TranslationCacheKey MakeTranslationCacheKey(String blob) {
|
|
TranslationCacheKey key;
|
|
key.hash = static_cast<Uint64>(XXH64(blob.data(), blob.size(), 0));
|
|
key.blob = MakeShared<const String>(Move(blob));
|
|
return key;
|
|
}
|
|
|
|
TranslationCacheKey BuildSpirvTranslationKey(const SpirvTranslationKeyInputs& inputs) {
|
|
TranslationKeyBuilder builder;
|
|
AppendCommonKeyPrefix(builder, kSpirvKeyTag);
|
|
builder.Value(inputs.frontendFingerprint);
|
|
builder.Value(inputs.shaderCompileFlags);
|
|
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
|
builder.Value(static_cast<Uint64>(inputs.stages.size()));
|
|
for (const auto& stage : inputs.stages) {
|
|
builder.Value(static_cast<Uint32>(stage.type));
|
|
builder.Text(stage.preprocessedSource);
|
|
}
|
|
static const UnorderedMap<String, Uint> kEmpty;
|
|
builder.NameMap(inputs.explicitVertexInLocations ? *inputs.explicitVertexInLocations : kEmpty);
|
|
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);
|
|
}
|
|
|
|
TranslationCacheKey BuildShaderParseVerdictKey(const ShaderParseVerdictKeyInputs& inputs) {
|
|
TranslationKeyBuilder builder;
|
|
AppendCommonKeyPrefix(builder, kParseVerdictKeyTag);
|
|
builder.Value(inputs.frontendFingerprint);
|
|
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
|
builder.Value(inputs.shaderCompileFlags);
|
|
builder.Text(inputs.preprocessedSource);
|
|
return MakeTranslationCacheKey(builder);
|
|
}
|
|
|
|
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) { return verdict.infoLog.size(); }
|
|
|
|
// Leaked for the same exit-order reason as the other two; see the note below.
|
|
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
|
|
static auto* const kCache = new BoundedTranslationCache<ShaderParseVerdict>(
|
|
"ShaderTranslationCache L1c (GLSL->parse verdict)", kParseVerdictCacheMaxEntries,
|
|
kParseVerdictCacheMaxBytes);
|
|
return *kCache;
|
|
}
|
|
|
|
TranslationCacheKey BuildEsslTranslationKey(const EsslTranslationKeyInputs& inputs) {
|
|
TranslationKeyBuilder builder;
|
|
AppendCommonKeyPrefix(builder, kEsslKeyTag);
|
|
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
|
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
|
|
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
|
builder.Value(inputs.maxColorTextureSamples);
|
|
builder.Value(inputs.maxIntegerSamples);
|
|
builder.Value(inputs.maxDepthTextureSamples);
|
|
builder.Value(inputs.advertisedMaxSamples);
|
|
builder.Value(static_cast<Uint32>(inputs.esslVersion));
|
|
builder.Value(inputs.atomicCounterEsslBindingTop);
|
|
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
|
static const std::set<String> kEmptySet;
|
|
builder.NameSet(inputs.xfbCaptureBlockNames ? *inputs.xfbCaptureBlockNames : kEmptySet);
|
|
static const UnorderedMap<String, Uint> kEmptyFormats;
|
|
builder.NameMap(inputs.glFormatByUniformName ? *inputs.glFormatByUniformName : kEmptyFormats);
|
|
static const UnorderedMap<String, Int> kEmptyBindings;
|
|
builder.NameMap(inputs.storageBlockBindingOverrides ? *inputs.storageBlockBindingOverrides
|
|
: kEmptyBindings);
|
|
static const Vector<Uint32> kEmptyWords;
|
|
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
|
|
return MakeTranslationCacheKey(builder);
|
|
}
|
|
|
|
SizeT EsslTranslationResultBytes(const EsslTranslationResult& result) {
|
|
SizeT bytes = result.essl.size();
|
|
for (const String& name : result.flattenedXfbBlockNames) bytes += name.size();
|
|
bytes += result.atomicCounterGlBindings.size() * sizeof(Int);
|
|
return bytes;
|
|
}
|
|
|
|
// BOTH SINGLETONS ARE DELIBERATELY LEAKED, and this is not a style choice - it is the
|
|
// fix for a crash that reproduced 25 times in 40 runs of AsyncCompileTest.
|
|
//
|
|
// A plain function-local static object registers its destructor with __cxa_atexit AT
|
|
// FIRST USE, and first use here is a ShaderCompilePool worker running the first phase B.
|
|
// ShaderCompilePool registers its own atexit drain sentinel at FIRST POOL USE, which is
|
|
// strictly earlier - and exit handlers run in REVERSE registration order. So the cache
|
|
// would be destroyed FIRST, while workers are still live, and the next worker to reach
|
|
// Insert() would write into a freed std::list and a freed mutex. The observed symptom
|
|
// was not a crash in the cache at all: it was heap corruption surfacing later, inside
|
|
// spirv-tools' AggressiveDCEPass destructor on the worker thread.
|
|
//
|
|
// This is the same exit-order hazard PinValidatorTablesForProcessExit documents in
|
|
// ShaderCompiler.cpp for the validator's lazily-built tables, arriving by the same
|
|
// route. Pinning the construction order the way that function does would work too, but
|
|
// leaking is stronger: it holds however late the first phase B happens to run, and a
|
|
// process-lifetime memo has nothing to release at exit that the OS will not reclaim.
|
|
//
|
|
// 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<EsslTranslationResult>& GetEsslTranslationCache() {
|
|
static auto* const kCache = new BoundedTranslationCache<EsslTranslationResult>(
|
|
"ShaderTranslationCache L2 (SPIR-V->ESSL)", kEsslCacheMaxEntries, kEsslCacheMaxBytes);
|
|
return *kCache;
|
|
}
|
|
|
|
void ClearShaderTranslationCaches() {
|
|
GetShaderParseVerdictCache().Clear();
|
|
GetEsslTranslationCache().Clear();
|
|
}
|
|
|
|
void LogShaderTranslationCacheStats() {
|
|
GetShaderParseVerdictCache().LogStats();
|
|
GetEsslTranslationCache().LogStats();
|
|
}
|
|
} // namespace MobileGL::MG_Util::ShaderTranspiler
|