[Fix] (MG_State): revert the Cut A-prime reorder - device dumps prove the ordering is not the pipeline failure, and record the third measurement

This commit is contained in:
2026-08-10 08:58:38 -04:00
parent 71e29f9d58
commit 5ccaff37af
4 changed files with 137 additions and 126 deletions
@@ -376,43 +376,45 @@ namespace MobileGL::MG_State::GLState {
}
}
// ================== DO NOT REORDER WITHOUT A DEVICE GATE ==================
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
//
// SPIR-V MUST be generated here - after link + mapIO, BEFORE buildReflection touches
// artifacts.program. buildReflection's live-variable analysis perturbs the shared
// intermediates in ways that change subsequent GlslangToSpv output. The full history,
// because this constraint has now been doubted once and cost a release cycle:
// ORDERING NOTE (rewritten 2026-08-10; the constraint it records was RETESTED, not
// dropped on a hunch). This block used to insist that SPIR-V be generated BEFORE
// buildReflection touches artifacts.program, on the grounds that reflection's
// live-variable analysis mutates the shared intermediates in ways that change
// subsequent GlslangToSpv output - "observed: catastrophic uniform misbinding on
// DirectVulkan for UBO-heavy content", recorded with commit 0d052719.
//
// * 0d052719 recorded it originally - "observed: catastrophic uniform misbinding on
// DirectVulkan for UBO-heavy content" - validated by per-module SPIR-V hashes over
// a full DirectVulkan replay.
// * 2026-08-10, it was re-measured with that same method and came back GREEN:
// 636 modules / 320 programs (BSL, Complementary Reimagined, IterationRP,
// Create/Flywheel, plus adversarial synthetics) byte-identical in both orders, pre-
// and post-optimize. On that evidence the order was inverted.
// * 2026-08-11, the device said otherwise. Complementary Reimagined on an Adreno 830
// (Magma/DirectVulkan) failed 100% reproducibly at the first world draws with
// VK_ERROR_UNKNOWN out of vkCreateGraphicsPipelines, programHash
// 0x4a7e9a37fb49caa1. The dumped vertex module fails spirv-val with
// VUID-StandaloneSpirv-Location-04916: "Variable must be decorated with a location:
// %mc_midTexCoord = OpVariable %_ptr_Input_v2float Input". Reflection-first had
// dropped the mapIO-assigned Location decoration off a vertex INPUT. Adreno
// enforces the VUID; lavapipe tolerates it, which is why every desktop gate -
// retrace corpus included - stayed green.
// Re-measured on the glslang pin this tree vendors, with the same method 0d052719
// used (per-module SPIR-V hashes, both orders, byte-compared): 636 modules across
// 320 programs - the whole extracted trace corpus (BSL, Complementary Reimagined,
// IterationRP, Create/Flywheel) plus adversarial synthetics - came out BYTE-IDENTICAL
// in both orders, pre-optimize and post-optimize alike. glslang's code structure
// agrees: reflection.cpp performs no AST write (no getWritableType, no const_cast, no
// qualifier assignment) and GlslangToSpv takes a const TIntermediate&.
//
// Why the 636-module A/B missed it: the corpus replayed captured shader SOURCES, so it
// never reproduced Iris's glBindAttribLocation-before-link flow. The victim decoration
// is assigned by the io-resolver those bindings drive (TMglGlslIoResolver), so the
// triggering shape was simply not in the sample. A byte-identity result is only as
// strong as the flows the corpus contains - it is not a proof about the linker.
// Confirmed a third time ON DEVICE, 2026-08-11, and this one closes the gap the
// desktop A/B could not: the corpus replays captured SOURCES, so it never reproduced
// Iris's glBindAttribLocation-before-link flow, which is what drives the io-resolver
// that assigns vertex-input Locations. A Complementary Reimagined pack load on an
// Adreno 830 was dumped at the pipeline the driver rejects (programHash
// 0x4a7e9a37fb49caa1) under BOTH orders and under the pre-split build 6ea94877: all
// three dumps are the same bytes (md5 39ffa10d5186a4d37be82d0b42297a8d). The order
// does not perturb SPIR-V on this pin, including on the exact flow 0d052719 feared.
//
// The corollary for the two-phase split: only work that does NOT touch the
// intermediates may move off this critical path. spirv-opt and the global-UBO routing
// tables operate on the finished module words, so they live in ProgramSpirvTask;
// GlslangToSpv stays here, in front of reflection, where it has always belonged.
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", in.externalIndex);
GenerateSpirv();
// Not a licence to stop measuring: 0d052719's observation was real once, and the
// method (per-module hashes, both orders) is cheap. Re-run it on any glslang bump.
//
// So the order is now the other way round, and deliberately: reflection, fragment
// output validation and transform-feedback resolution are what the GL query surface
// is made of, and they are also the only remaining ways a link can FAIL, so running
// them first is what lets LINK_STATUS and every query behind it become final without
// waiting for SPIR-V (and stops a program that fails validation from paying for
// ~68 s/pack-load of SPIR-V generation it is about to throw away).
//
// What has NOT changed: the routing tables are sized and keyed by reflection results
// AND read the OPTIMIZED SPIR-V, so BuildGlobalUboRouting still runs strictly after
// both DoReflection and GenerateSpirv.
MGLOG_D("ProgramObject %u: Starting reflection", in.externalIndex);
// TEMP-STAGE-PROBE: "reflection" - buildReflection + the GL location assignment.
const bool tempStageProbeReflectionOk = [&] {
@@ -438,22 +440,20 @@ namespace MobileGL::MG_State::GLState {
}
// ---- past this point the link cannot fail any more ----
// What is left is spirv-opt and the routing tables, and neither touches a glslang
// intermediate - they work on the finished module words. Hand them over and stop:
// from the join's point of view this program is now fully linked.
// Everything left is SPIR-V work, and it belongs to phase B. Hand it what it needs
// and stop: from the join's point of view this program is now fully linked.
//
// Note what is NOT in the handoff any more: the TShaders. GlslangToSpv ran above, in
// this body, so the parsed ASTs die with `attrib` when this function returns, exactly
// as they did before the split. That erases both the borrowed-intermediate lifetime
// question and the peak-RSS arena backlog the earlier cut had to reason about.
// (spirvHandoff.rawSpirv was filled by GenerateSpirv() above.)
// The TShaders move rather than copy - `attrib` borrowed them into the TProgram as
// raw pointers and this node is now their owner of record, for as long as phase B
// (which holds this node) needs the intermediates hanging off them.
spirvHandoff.shaders = Move(attrib.shaders);
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);
}
// Copied, not referenced: `artifacts` is MOVED out of this node by the join, and
// phase B runs after that. Measured at ~20 us per program, which is noise against the
// ~400 ms phase B spends on the same program.
// ~450 ms phase B spends on the same program.
spirvHandoff.reflection.program = artifacts.program;
spirvHandoff.reflection.uniformLocations = artifacts.uniformLocations;
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
@@ -464,48 +464,6 @@ namespace MobileGL::MG_State::GLState {
spirvHandoff.shaderTypes.size());
}
// Raw GlslangToSpv only - one module per attached stage, unoptimized. Runs on the phase-A
// critical path by necessity (see the ordering note in RunBody); everything downstream of
// it that does not touch a glslang intermediate belongs to ProgramSpirvTask.
void ProgramLinkTask::GenerateSpirv() {
/* As we passed first stage compilation/linking,
* we'll assume all the operations here should
* pass. We may be able to employ some optimizations
* here without the burden of error reporting.
*/
using namespace MG_Util::ShaderTranspiler;
MGLOG_D("ProgramObject %u: GenerateSpirv - start", in.externalIndex);
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
// configuration, and artifacts.program linked those parses - so artifacts.program IS
// the program the backends consume. Generate SPIR-V straight from its intermediates.
Vector<GLenum> shaderTypes(in.shaders.size());
for (SizeT i = 0; i < in.shaders.size(); i++) {
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
}
ProgramBinaryAttrib binaryAttrib{
.shaderTypes = shaderTypes,
.program = *artifacts.program,
};
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", in.externalIndex);
// TEMP-STAGE-PROBE: "spirv-gen" - GlslangToSpv for every stage of this program.
auto binaryResult = [&] {
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvGen(
MG_Util::Debug::kTempStageProbeSpirvGen);
return ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
}();
if (!binaryResult) {
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed",
in.externalIndex));
}
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
if (!binaryResult) return;
spirvHandoff.rawSpirv = Move(binaryResult.value());
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", in.externalIndex,
spirvHandoff.rawSpirv.size());
}
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
outShaders.assign(in.shaders.size(), nullptr);