Files
MobileGL/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp
T
swung0x48 fdd611e83d [MG_Backend, MG_Test, MG_IntegrationTest, scripts, docs] (P7): the depth mip chain is baked, and every baked header in the tree now has to match its source
CONTRACT-P7 §5.1's (A) and (D), and (D) is what makes (A) safe to have.

(A) WireDepthMipmap.{vert,frag,inc}. VK_FORMAT_FEATURE_BLIT_DST is OPTIONAL for depth
formats, so a depth chain through a shader is the ordinary case and not the exotic
one - the monolith arm has carried such a fallback since before the split. The wire
arm had none, so a depth texture the device would not blit died on the first
disjunction of `mipmap-shader-format-or-shape`, and a DEPTH|STENCIL one died a guard
earlier as `depth-stencil-mipmap@P7` whether or not it could be blitted.

The monolith's fallback is not reusable, which is the whole of (A): it builds a GL
ProgramObject, two ShaderObjects and a GL sampler, binds through the uniform manager's
global UBO and writes uSrcTexelSize as a default-block uniform - four frontend objects
and a GL compiler on the server's apply thread. The baked program is the same shader
with the same arithmetic on a push constant and its own descriptor set. The fragment
stage is a PORT and not a rewrite: §3.2 retires a refusal by making the wire arm
observably the monolith's, so the 2x2 box lands where the monolith's lands, including
the half texel its ivec2(texCoord * srcTexelSize) sits off the destination centre.

`depth-stencil-mipmap@P7` therefore splits. DEPTH-only retires into the baked program.
DEPTH|STENCIL declines - MGLOG_E_ONCE and return, before the chain is grown, so a level
this verb will not fill is not created and then recorded as defined. A decline rather
than a Fatal because the monolith does no better: its fallback asserts depth-only, and
an assert is nothing in the release build both arms ship as. Nothing in GL promises a
stencil mip either, and this pass writes gl_FragDepth - it would leave the stencil
aspect of the level it just produced undefined.

(D) MOBILEGL_BAKED_INTERNAL_SHADERS. A baked header is a compiler output committed with
its input beside it and nothing connecting the two: edit the source and the build stays
green while the shader that runs is the one from before the edit. MG_Test's
BakedInternalShadersTest recompiles all eight arrays - WireColorBlit x2,
WireDepthMipmap x2, the iterationRP witness, the three probe stages - from their
committed GLSL with the in-tree glslang and compares word for word.

THE GATE IS THE BAKER. scripts/bake_internal_shaders.py compiles nothing; it runs this
binary with MOBILEGL_BAKE_INTERNAL_SHADERS and splices the words it emits. A gate that
used the glslang LIBRARY while the baker shelled out to a glslangValidator BINARY would
be two compilers with no rule about which is right, and the day their defaults diverge
the gate reds on a header nobody can regenerate.

What the first run measured, which is the one thing nobody could know beforehand:
WireColorBlitSpirv.h and all three arrays of PrimitivesGeneratedNoXfbProbeSpv.h were
already fresh - the hand-run glslangValidator words reproduce exactly. The iterationRP
witness was stale, as its own prose said; it is regenerated here (2142 -> 2143 words),
and the witness magic 0x50323033 is still there because it is a constant in the .comp
rather than a property of the words. All three provenance comments now point at the
script and the gate instead of at a tool on somebody's PATH.

Scenarios, on the three Magma arms: GenerateMipmapDepthWithoutNativeBlitPixels runs
DEPTH_COMPONENT24 and DEPTH_COMPONENT32F under MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1 and
reads level one back through a real sampled attachment, asserting per half and leaving
row 3 - the straddle band - unpinned, because pinning it would assert the half-texel
offset rather than the mip. GenerateMipmapDepthStencilDeclinesAndKeepsTheSession takes
no knob (the decline is on the aspect, before blit support is asked about) and pins that
the session survives and level zero still holds what was cleared into it.

Red once, three of them, executed and reverted:
 - a one-character edit to WireDepthMipmap.frag (no rebuild - the gate reads the source
   at runtime) reds the WireDepthMipmapFragment row at word 279;
 - with the depth arm disabled, the DepthMip entries die with Fatal{UnmigratedVerb,
   "Magma:mipmap-shader-format-or-shape format=125 ... aspect=0x2 ...
   missing=COLOR_ATTACHMENT"} - format 125 is X8_D24_UNORM_PACK32, which slice 1's
   diagnostic names for us;
 - with the Fatal restored, the decline case dies as
   Fatal{UnmigratedVerb, "Magma:depth-stencil-mipmap@P7"}.

Gates: integration-magma-split 77 (+2), -spawn 56 (+2), -tcp 58 (+2), 0 red;
ctest -L unit 2410 (+8, the gate's rows); informational integration-magma-full-split
513, 0 red, 59 skip (base 510/0/57); fatal_census green (79 abort sites, 43 words, 0
unmarked); link_ratchet --assert-monotone unchanged at 186, p7-magma 101.

G1 holds and it was not obvious: the witness header is compiled into the pull build
too, and its word count changed. Pull .text is still 0xa52203 and the defined-symbol
name list is identical to ~/w7/p7-before/pull-syms.txt line for line (30570) - the
change lands in .rodata and widens no instruction encoding.

VulkanRenderer.h is edited outside this package's file list: the new pass needs four
private declarations next to the colour blit's. Flagged for the integrator.
2026-09-22 16:37:54 -04:00

227 lines
12 KiB
C++

// MobileGL - MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp
// Copyright (c) 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
//
// MOBILEGL_BAKED_INTERNAL_SHADERS - the freshness gate (CONTRACT-P7 §5.1 (D)).
//
// WHAT A BAKED SPIR-V HEADER ACTUALLY IS: a compiler output committed to the tree, with its
// input committed beside it and NOTHING connecting the two. Edit WireColorBlit.frag and the
// build stays green, the lane stays green, and the shader that runs is the one from before
// the edit - silently, on every device, until somebody reads the words. The tree already had
// three such pairs before this package added a fourth, and one of them (the iterationRP
// witness) was measurably stale: its own header said so in prose, which is exactly as much
// enforcement as prose provides.
//
// So every baked header is recompiled here, from its committed source, with the IN-TREE
// glslang, and compared word for word.
//
// THIS TEST IS THE BAKER, NOT A SECOND OPINION. scripts/bake_internal_shaders.py does not
// compile anything: it runs this binary with MOBILEGL_BAKE_INTERNAL_SHADERS set, takes the
// words it emits, and splices them into the headers. That direction is deliberate. A gate
// that compiles through the glslang LIBRARY while the baker shells out to a glslangValidator
// BINARY has two compilers and no rule about which one is right, and the day their defaults
// diverge - an option the CLI passes by default, a version skew between a system tool and
// the submodule - the gate reds on a header nobody can regenerate. One compiler, one truth,
// and the gate's own output is what the tree stores.
//
// SOURCE PATH: the sources live in the source tree, not next to the binary, so the build
// hands their root in as MOBILEGL_BAKED_SHADER_ROOT. A test that cannot find them FAILS
// rather than skipping: a freshness gate that silently disarms is the thing it exists
// against.
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <glslang/Public/ResourceLimits.h>
#include <glslang/Public/ShaderLang.h>
#include <glslang/SPIRV/GlslangToSpv.h>
#include <MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.h>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h>
namespace {
// The two Renderer headers are written to be included inside the backend namespace, after
// the project's own type aliases; they name Uint32 and declare nothing else. Including them
// here with a local alias keeps them byte-identical to what VulkanRenderer.cpp compiles -
// the alternative, a copy of the words in this file, is precisely the drift this gate is for.
using Uint32 = std::uint32_t;
#include <MG_Backend/DirectVulkan/Renderer/WireColorBlitSpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireDepthMipmapSpirv.h>
struct BakedShader {
const char* name; // ctest entry name, and the bake file's stem
const char* source; // repository-relative GLSL source
const char* header; // repository-relative header the words live in
const char* symbol; // the array identifier inside that header
EShLanguage stage;
glslang::EShTargetClientVersion client;
glslang::EShTargetLanguageVersion spv;
const std::uint32_t* words;
std::size_t wordCount;
};
#define MGL_BAKED_ROW(name, source, header, symbol, stage, client, spv, array) \
BakedShader{name, source, header, symbol, stage, client, spv, (array), \
sizeof(array) / sizeof((array)[0])}
const BakedShader kBakedShaders[] = {
// The Magma wire arms' own programs: no GL program object, no registry entry, no runtime
// transpilation, so the words in the header are the only copy that exists.
MGL_BAKED_ROW("WireColorBlitVertex",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.vert",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlitSpirv.h",
"kWireColorBlitVertexSpirv", EShLangVertex,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireColorBlitVertexSpirv),
MGL_BAKED_ROW("WireColorBlitFragment",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.frag",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlitSpirv.h",
"kWireColorBlitFragmentSpirv", EShLangFragment,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireColorBlitFragmentSpirv),
MGL_BAKED_ROW("WireDepthMipmapVertex",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.vert",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmapSpirv.h",
"kWireDepthMipmapVertexSpirv", EShLangVertex,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireDepthMipmapVertexSpirv),
MGL_BAKED_ROW("WireDepthMipmapFragment",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.frag",
"MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmapSpirv.h",
"kWireDepthMipmapFragmentSpirv", EShLangFragment,
glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0,
kWireDepthMipmapFragmentSpirv),
// The two driver self-test probes. They are not Magma's, and they are in this table for
// the reason the contract names them: they are baked, so they can be stale, and one of
// them was.
MGL_BAKED_ROW("DriverPostIterationRPWitness",
"MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.comp",
"MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.h",
"kDriverPostIterationRPWitnessSpv", EShLangCompute,
glslang::EShTargetVulkan_1_1, glslang::EShTargetSpv_1_3,
MobileGL::MG_Util::SelfTest::kDriverPostIterationRPWitnessSpv),
MGL_BAKED_ROW("PrimitivesGeneratedNoXfbProbeVert",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h",
"kPrimitivesGeneratedNoXfbProbeVertSpv", EShLangVertex,
glslang::EShTargetVulkan_1_1, glslang::EShTargetSpv_1_3,
MobileGL::MG_Util::SelfTest::kPrimitivesGeneratedNoXfbProbeVertSpv),
MGL_BAKED_ROW("PrimitivesGeneratedNoXfbProbeTesc",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h",
"kPrimitivesGeneratedNoXfbProbeTescSpv", EShLangTessControl,
glslang::EShTargetVulkan_1_1, glslang::EShTargetSpv_1_3,
MobileGL::MG_Util::SelfTest::kPrimitivesGeneratedNoXfbProbeTescSpv),
MGL_BAKED_ROW("PrimitivesGeneratedNoXfbProbeTese",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese",
"MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h",
"kPrimitivesGeneratedNoXfbProbeTeseSpv", EShLangTessEvaluation,
glslang::EShTargetVulkan_1_1, glslang::EShTargetSpv_1_3,
MobileGL::MG_Util::SelfTest::kPrimitivesGeneratedNoXfbProbeTeseSpv),
};
#undef MGL_BAKED_ROW
std::string RepositoryRoot() {
#ifdef MOBILEGL_BAKED_SHADER_ROOT
return std::string(MOBILEGL_BAKED_SHADER_ROOT);
#else
return std::string();
#endif
}
// glslang::InitializeProcess is process-wide and refcounted. One call for the whole binary:
// this test never runs MobileGL::Initialize, which is the other caller.
void EnsureGlslangProcess() {
static const bool ready = glslang::InitializeProcess();
ASSERT_TRUE(ready) << "glslang::InitializeProcess failed";
}
class BakedInternalShaders : public ::testing::TestWithParam<BakedShader> {};
TEST_P(BakedInternalShaders, HeaderMatchesItsSource) {
const BakedShader& row = GetParam();
ASSERT_NO_FATAL_FAILURE(EnsureGlslangProcess());
const std::string root = RepositoryRoot();
ASSERT_FALSE(root.empty())
<< "MOBILEGL_BAKED_SHADER_ROOT was not defined for this target: the gate cannot find "
"the GLSL sources and must not pass by default";
const std::string sourcePath = root + "/" + row.source;
std::ifstream in(sourcePath, std::ios::binary);
ASSERT_TRUE(in.good()) << "cannot open " << sourcePath;
std::ostringstream buffer;
buffer << in.rdbuf();
const std::string source = buffer.str();
ASSERT_FALSE(source.empty()) << sourcePath << " is empty";
glslang::TShader shader(row.stage);
const char* strings[] = {source.c_str()};
shader.setStrings(strings, 1);
shader.setEnvInput(glslang::EShSourceGlsl, row.stage, glslang::EShClientVulkan, 100);
shader.setEnvClient(glslang::EShClientVulkan, row.client);
shader.setEnvTarget(glslang::EShTargetSpv, row.spv);
const EShMessages messages = static_cast<EShMessages>(EShMsgSpvRules | EShMsgVulkanRules);
ASSERT_TRUE(shader.parse(GetDefaultResources(), 100, false, messages))
<< row.source << " does not compile:\n" << shader.getInfoLog();
glslang::TProgram program;
program.addShader(&shader);
ASSERT_TRUE(program.link(messages)) << row.source << " does not link:\n" << program.getInfoLog();
glslang::TIntermediate* intermediate = program.getIntermediate(row.stage);
ASSERT_NE(intermediate, nullptr);
std::vector<unsigned int> spirv;
spv::SpvBuildLogger logger;
glslang::SpvOptions options;
options.disableOptimizer = true;
glslang::GlslangToSpv(*intermediate, spirv, &logger, &options);
ASSERT_FALSE(spirv.empty()) << "GlslangToSpv produced nothing for " << row.source;
// Bake mode writes what it computed and asserts nothing else: the script that sets this
// variable is about to overwrite the header, so a red here would only stop it doing so.
if (const char* bakeDir = std::getenv("MOBILEGL_BAKE_INTERNAL_SHADERS")) {
const std::string out = std::string(bakeDir) + "/" + row.symbol + ".words";
std::ofstream sink(out, std::ios::binary);
ASSERT_TRUE(sink.good()) << "cannot write " << out;
sink << row.header << "\n";
for (unsigned int word : spirv) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x\n", word);
sink << text;
}
ASSERT_TRUE(sink.good()) << "failed writing " << out;
return;
}
ASSERT_EQ(spirv.size(), row.wordCount)
<< row.header << ": " << row.symbol << " has " << row.wordCount << " words, recompiling "
<< row.source << " with the in-tree glslang produces " << spirv.size()
<< ". Regenerate with scripts/bake_internal_shaders.py.";
for (std::size_t at = 0; at < spirv.size(); ++at) {
ASSERT_EQ(spirv[at], row.words[at])
<< row.header << ": " << row.symbol << " word " << at << " is stale against "
<< row.source << ". Regenerate with scripts/bake_internal_shaders.py.";
}
}
INSTANTIATE_TEST_SUITE_P(BakedShaders, BakedInternalShaders,
::testing::ValuesIn(kBakedShaders),
[](const ::testing::TestParamInfo<BakedShader>& info) {
return std::string(info.param.name);
});
} // namespace