mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Perf] (ShaderTranspiler): memoize a linked program's sanitized SPIR-V (translation cache L1)
This commit is contained in:
@@ -497,11 +497,53 @@ namespace MobileGL::MG_State::GLState {
|
||||
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
||||
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
||||
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
||||
spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env);
|
||||
spirvHandoff.ready = true;
|
||||
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
||||
spirvHandoff.shaderTypes.size());
|
||||
}
|
||||
|
||||
// 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).
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
|
||||
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
if (!ShaderTranslationCacheEnabled()) return {};
|
||||
|
||||
SpirvTranslationKeyInputs keyInputs;
|
||||
keyInputs.envFingerprint = env.fingerprint;
|
||||
// Always 0 on both production parse paths (ShaderCompileTask::RunCompilePipeline and
|
||||
// ClaimParsedShader's re-parse). In the key regardless, so that a future non-zero
|
||||
// value cannot alias a module parsed without it.
|
||||
keyInputs.shaderCompileFlags = 0;
|
||||
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
|
||||
keyInputs.stages.reserve(in.shaders.size());
|
||||
for (const LinkShaderInput& shader : in.shaders) {
|
||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||
if (compiled.preprocessedSource.empty()) {
|
||||
// No text to key on - an internal shader object, or an artifact this build
|
||||
// did not populate. Refuse to key rather than key on nothing.
|
||||
return {};
|
||||
}
|
||||
keyInputs.stages.push_back(SpirvTranslationKeyInputs::Stage{
|
||||
.type = MG_Util::ConvertShaderStageToGLEnum(shader.stage),
|
||||
.preprocessedSource = StringView(compiled.preprocessedSource)});
|
||||
}
|
||||
if (keyInputs.stages.empty()) return {};
|
||||
keyInputs.explicitVertexInLocations = &in.explicitAttribLocations;
|
||||
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
|
||||
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
|
||||
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
|
||||
return BuildSpirvTranslationKey(keyInputs);
|
||||
}
|
||||
|
||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||
outShaders.assign(in.shaders.size(), nullptr);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
|
||||
#include <MG_Util/Async/JobNode.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// One attached shader, as the link sees it: never the ShaderObject, always a snapshot.
|
||||
@@ -116,6 +117,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
// for phase B after the join has moved `artifacts` away.
|
||||
ProgramObject::LinkArtifacts reflection;
|
||||
|
||||
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||
// A, and not by phase B - two reasons, both structural:
|
||||
// * the key covers the four link-time request maps and the merged opaque
|
||||
// bindings, and one of those (explicitOpaqueUniformBindings) lives in
|
||||
// `artifacts`, which phase B is forbidden to read because the GL-thread join
|
||||
// moves it out from under phase B;
|
||||
// * built once, it serves both the lookup and the insert, so the program's
|
||||
// sources are copied into the blob exactly once per link.
|
||||
// Invalid (null blob) when the cache is disabled, or when a stage arrived
|
||||
// without preprocessed source - in which case phase B simply translates.
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey spirvCacheKey;
|
||||
|
||||
// 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;
|
||||
@@ -143,6 +157,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// 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.
|
||||
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
|
||||
|
||||
// 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.
|
||||
// Called at the tail of RunBody, where every input it needs is still owned by this
|
||||
// node and `artifacts` has not yet been published.
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey BuildSpirvCacheKey(
|
||||
const MG_Util::ShaderTranspiler::CompileEnv& env) const;
|
||||
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
|
||||
Bool ValidateFragmentOutputLocations();
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <cstring>
|
||||
@@ -152,6 +153,28 @@ 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
|
||||
@@ -195,6 +218,16 @@ 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,
|
||||
|
||||
Reference in New Issue
Block a user