Files
MobileGL/docs/Disaggregated/notes/p7/magma-c.md
T
swung0x48 8a445a3098 [docs] (P7): every cited feat/disaggregated commit is the rewritten one without the attribution trailer
- 144 commits from 785fed0b0 onward were rewritten to drop the Co-Authored-By trailer the
  project forbids; origin/feat/disaggregated moved from 1135c664 to 3c80cd62 (same trees)
- the old->new map is ~/w7/logs/sha-map-20260922.txt; every 7..40-hex citation in the
  Disaggregated docs and the MG_Remote contracts that matched an old SHA is replaced by the
  same-length prefix of its rewrite; no other content changes
2026-09-22 20:51:15 -04:00

27 KiB
Raw Blame History

P7 wave 2 package C — Magma object death, resident sub-data, the reflection archive, the vertex-layout split

For the integrator, first

Nine things this package changes that live outside its own files or outside its own slices. CONTRACT-P7.md is the integrator's file (§0 "怎么改"), so none of these edit it.

  1. §12's debt row is retired. "Magma 两进程 client 在 StateObjectDeathOps 落地前 ObjectDeaths=0;CtWireScenario 的死亡用例在 Magma 臂按名 skip" — the table landed, both skip guards are deleted, and the two cases are green on DirectVulkan.{Split,Spawn,Tcp}. §2.3's "Magma 自己的 StateObjectDeathOps 表(wave 2-C)落地前…按名 skip" can go with it.
  2. §3.1's pinned census moves: 15 sites / 4 files → 13 / 3. Renderer/WireDraw.inc leaves the list entirely. scripts/ci/fatal_census.py is unchanged (79 / 43 / 3 / 0) — the funnels were already converted in wave 0, so retiring a MagmaWireFatal CALL moves no abort count.
  3. §3.2 says "the other six" for vertex-layout; there are seven. offset-overflow post-dates the audit. It is masked with the rest; the reasoning is in slice 4 below.
  4. §5.4's white-box case (opcode 49 on the wire) is not achievable at this tree, and the blocker is R-6, not this package. The full chain is in slice 2. The capability is wired ahead of its consumer on purpose; rsd= on the PipeStats line is what will show the records the day P11 lands an adoption tier.
  5. §5.3's g_programResourceCaches deletion is achieved in the disaggregated build only. The archive member has to be #if MOBILEGL_BUILD_DISAGGREGATED for G1, so the pull build keeps the cache. One-line change the day that pin is retired. P13.
  6. A family-word question. buffer-window is now a permanent protocol error going through MagmaWireFatal, whose family is UnmigratedVerb. The honest word is ProtocolCorruption; MG_Pipe::MGPipeFatalFamily carries only two words and its own comment says a third is where the choice gets argued. §3.3 keeps the funnels on UnmigratedVerb deliberately so the refusal census stays comparable to P5b. Flagged, not decided.
  7. Gating lane counts grow (grow-only, G14-legal): integration-magma-split 73 → 75, -spawn 52 → 54, -tcp 54 → 56, integration-split 186 → 187, integration-spawn 102 → 103, ctest -L unit 2402 → 2403.
  8. One ratchet symbol changes bucket without changing the total (186, monotone green): p3b-p4b-espryt 14 → 13 and both-backends 60 → 61, because slice 3's monolith consumer makes DirectVulkan.cpp reference a frontend symbol only DirectGLES referenced before. p7-magma — P7's own target bucket — is unchanged at 101. Nothing in this package was aimed at gate 4; the §4.2 path to 101 → 0 is package B's (B').
  9. The worktree tool leaves MOBILEGL_ITEST_VK_ICD empty and CMake warns about it; every Magma entry then runs whatever ICD the loader finds first. This package reconfigured build-split with /usr/share/vulkan/icd.d/lvp_icd.json, the pin ~/w7/pipe/build-split already carries. Worth adding to p7_worktree.sh so the next package does not have to notice it.

Base: p7/magma-c off feat/disaggregated @ 4f2d4c4e (pipe HEAD at branch time). Build: build-split = -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_INTEGRATION_TEST=ON, ICD pinned to /usr/share/vulkan/icd.d/lvp_icd.json (the same lavapipe ICD ~/w7/pipe/build-split pins; the worktree tool leaves MOBILEGL_ITEST_VK_ICD empty and CMake warns about it).

Contract rows: CONTRACT-P7 §5.5 (StateObjectDeathOps), §5.4 (OQ-10), §5.3 (OQ-8), §3.2 (vertex-layout, vertex-format-conversion). Discipline: rule I (no std::abort() on a Magma wire arm), rule J / R-16 (every fix red once on a two-process arm first).

Gates, on the four slices together

gate base (4f2d4c4e) after
ctest -L unit 2402 2403 / 2403 +1 = the OQ-8 order case
integration-magma-split 73 (71 P + 2 S) 75 / 75, 0 skipped +2 vertex-layout; the 2 skips were the death cases
integration-magma-spawn 52 (50 + 2) 54 / 54, 0 skipped
integration-magma-tcp 54 (52 + 2) 56 / 56, 0 skipped
integration-split 186 187 / 187 +1 = the OQ-10 capability case
integration-spawn 102 103 / 103
integration-magma-full-split 453 P / 57 S / 0 red 513 entries, 0 red, 55 skipped → 458 P / 55 S better on both counts
scripts/ci/fatal_census.py 79 / 43 / 3 / 0 79 / 43 / 3 / 0 unchanged, as expected
scripts/ci/spawn_lane_parity.py green green all tiers equal after normalisation
grep -rn "@P7" 15 sites / 4 files 13 / 3 WireDraw.inc leaves the list
link_ratchet.py --assert-monotone 186 186, "unchanged" green
G1 (build-linux, pull) .text 0xa52203 .text 0xa52203 (10822147, +0) .data/.bss/.rodata/total all +0; 27837 → 27837 defined symbols, 0 added / 0 removed / 0 resized / 0 renamed; the .so is byte-identical on disk (19143824)

Ratchet buckets. AFTER: p7-magma 101, p3b-p4b-espryt 13, both-backends 61, p6-core 11, total 186. CONTRACT-P7 §4.1 records the 8bb6309a baseline as 101 / 14 / 60 / 11. So one symbol sits in both-backends that the baseline recorded under p3b-p4b-espryt; the total, the monotone assertion and P7's own target bucket (101) are all unchanged. HONESTY NOTE: I did not separately re-measure the buckets at 4f2d4c4e, so I cannot attribute that single move to this package rather than to the two commits between the baseline and this branch point. Nothing here was aimed at gate 4 — §4.2's path from 101 to 0 is package B's (B'). (An attempt to identify the mover by rebuilding one object at the base failed to compile and was abandoned; the clean way is --bucket --verbose-buckets on a base build.)


Slice 1 — Magma StateObjectDeathOps (§5.5)

What landed. MG_Backend/DirectVulkan/DirectVulkan.cpp gains g_magmaStateObjectDeathOps and InstallStateObjectDeathOps(), installed from BackendObject_DirectVulkan::Initialize() — i.e. from ServerLoop::CreateBackend, step 1 of InitServerRoleCommon, which both Magma server roles (inproc and the spawn/TCP child) run and which is before any client session exists. Guarded #if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH, so the pull build compiles nothing new (G1).

The one arm, and why it is only one. Espryt's g_glesStateObjectDeathOps has two jobs: emit the wire object_death record off the apply thread, and otherwise destroy a backend twin keyed by lifetime id. Magma has no lifetime-id-keyed twin at all — its two client-minted kinds (VertexElementsCso, Buffer) are keyed on {slot, gen} by MagmaPipeIdentityTables and the other six are still reached from their frontend objects — so the Magma table is exactly the EMIT arm and nothing else.

Correction to the plan's wording (worth the integrator's attention). Plan §1.3 and the task brief describe the gap as a client SLOT leak provable with PipeSlotPeek. It is not, and has not been since P4a: MG_Impl/Pipe/PipeFill.cpp's NotifyAndFree frees the client slot unconditionally and backend-neutrally, so PipeSlotPeek never saw a Magma leak and does not see a change now. What leaked was the server's twin. A framebuffer has no wire delete opcode at all (BRIEF-P4A D-I2), so object_death is its only death delivery; with no consumer installed under Magma inproc, the server's record stayed Live for the life of the session. The CtWireScenario framebuffer case now reads BOTH numbers — the server's deaths tally and the client's PipeSlotKind::Framebuffer live count — so a future regression in either half is attributable.

Cleanup of the Magma special cases. The two executable ones are the CtWireScenario skip guards (:153 / :209), both deleted. Everything else the audit counted is comment text, and it is reworded rather than deleted because the underlying argument survives: PipeSlotPeek.h, MagmaPipeArms.h (the age-sweep-instead-of-death-notice paragraph — still true, because the new table never touches the per-renderer identity tables), PipeMutation.h, PipeFill.cpp, VertexArrayObject.cpp, DirectGLES/Managers.cpp, HandleRecycleScenario.cpp, WireTables.cpp.

The install-order question, answered rather than assumed. SetStateObjectDeathOps is a bare last-writer-wins pointer with no stacking, so two installers could in principle race. They cannot meet: Magma's runs only in a process that OWNS a DirectVulkan backend, and InstallClientWireTables' arm runs only under Transport == Spawn, which is by construction the process that owns no backend (InitSplitRoles builds a BackendObject_Remote there). The red-once below measures exactly that split of responsibility.

Proof (green). ctest -R "DirectVulkan\.(Split|Spawn|Tcp)\.Ct\..*Death" — 6/6 pass (integration-magma-all-{split,spawn,tcp}), plus the two TcpServer fixtures. DirectGLES control ctest -R "DirectGLES\.(Split|Spawn|Tcp)\.Ct\..*Death" — 6/6 pass, unchanged.

RED-ONCE (executed). BackendObject_DirectVulkan::Initialize()'s call changed to if (false) InstallStateObjectDeathOps();, rebuilt, lane re-run:

1/8 DirectVulkan.Split.Ct.CtWireScenario.TextureDeath...        ***Failed
2/8 DirectVulkan.Spawn.Ct.CtWireScenario.TextureDeath...            Passed
4/8 DirectVulkan.Tcp.Ct.CtWireScenario.TextureDeath...              Passed
5/8 DirectVulkan.Split.Ct.CtWireScenario.FramebufferDeath...    ***Failed
6/8 DirectVulkan.Spawn.Ct.CtWireScenario.FramebufferDeath...        Passed
7/8 DirectVulkan.Tcp.Ct.CtWireScenario.FramebufferDeath...          Passed
75% tests passed, 2 tests failed out of 8

CtWireScenario.cpp:264: Failure
Expected: (afterCounters.deaths) > (deathsBefore), actual: 0 vs 0

Restored and rebuilt: 8/8 green again. This is precisely the shape package L's temporary probe predicted (notes/p7/magma-two-process-first-run.md §6): spawn and tcp were already carried by InstallClientWireTables' emitter, and inproc was the only hole — which is the hole this table closes.


Slice 2 — OQ-10 kCapResidentSubData (§5.4)

What landed. InitServerRoleCommon ORs kCapResidentSubData when g_resourceOpsAtStep2->SubDataResident != nullptr — the pointer that function already pinned at step 2 and that step 5 already refuses to see swapped. Never from ActiveBackendType (ID-39). rsd= joins the emit[] bracket on the PipeStats summary line, counted at MGPipeEmitBufferSubDataResident once per record. SplitRuntimePeek gains residentSubDataCap, read through the same CapsMirrorInstance().HasCap() accessor the frontend's MGPipeResourceOpsHaveSubDataResident reads.

BLOCKER FOR THE INTEGRATOR: §5.4's white-box case is not achievable at this tree

§5.4 asks for a case in integration-magma-buffers and one in integration-split proving that the emitted record is BufferSubDataResident (opcode 49). Neither can exist today, and the reason is R-6 rather than anything this package did or did not do. The chain:

step site verdict under split
the resident emitter runs only from a resident store BufferObject.cpp UploadSubData → if (m_resource.IsGpuResident()) —
residency comes only from AdoptPersistentMap PipeResource.h:181, the only writer of m_gpuMapped —
adoption needs a non-null map_persistent answer BufferObject.cpp TryAdoptLargeStorage / EnsureGpuResidentStorage / AcquireMemoryRange —
the answer is a decline PipeApply.cpp:2331 "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION, ALWAYS" always
the tier gate has no other implemented value PersistentMapTracker.cpp:1033 AdoptTierIsEmulate — T0/T1 are P11's and are a named Fatal{UnimplementedAdoptTier} always

So under any transport no buffer is ever GPU-resident, LandBytesIntoResidentStore is unreachable, and opcode 49 cannot cross. The capability is therefore wired ahead of its consumer, deliberately: when P11 lands a tier that adopts, the bit is already correct and already derived from the table, and rsd= is the counter that will show the records. The plan's phrasing ("both backends fall back to the in-place memcpy at BufferObject.cpp:~711-716") is one step optimistic — under split the code does not reach that fallback either, it takes the plain shadow Memcpy in UploadSubData above it.

What is proved instead, and it is both halves of the bit.

  • Server half — CapsMirrorTest.MagmaTransportPublishesRealBufferConsumersWithoutRunAhead (RemoteClientTest.cpp) now asserts (mask & kCapResidentSubData) != 0 == (ops->SubDataResident != nullptr). The two sides are compared to each other rather than both to true, so the case also covers a backend WITHOUT the arm on the day one exists.
  • Client half — the new CtWireScenario.TheServerPublishesTheResidentSubDataCapabilityFromIts OwnTable, registered through the ctCase list so tier 2 replays it: green on DirectGLES.{Split,Spawn,Tcp} (integration-{split,spawn,tcp}) and DirectVulkan.{Split,Spawn,Tcp} (integration-magma-all-*), 6/6.
  • Negative half — CapsMirrorTest.APlaceholderMirrorConsumesNothing keeps the no-snapshot-withholds-the-bit reading, which stopped being vacuous the moment anything set it.

RED-ONCE (executed, both levels). capBits |= MG_Pipe::kCapResidentSubData; → capBits |= 0;:

RemoteClientTest.cpp:692: Failure
Expected equality of these values:
  (server.CallMask() & static_cast<Uint64>(kCapResidentSubData)) != 0
    Which is: false
    Which is: true

25% tests passed, 6 tests failed out of 8
  DirectGLES.{Split,Spawn,Tcp}.Ct....ResidentSubDataCapability...   ***Failed
  DirectVulkan.{Split,Spawn,Tcp}.Ct....ResidentSubDataCapability... ***Failed
CtWireScenario.cpp:262: Failure
Value of: runtime.residentSubDataCap
  Actual: false

Restored: 11/11 unit and 6/6 integration green again.

One incidental finding, recorded because it cost a cycle. The first cut of the capability case did no GL work and failed on ScenarioFixture.h:90 — an armed split lane refuses a case whose encoder ordinal did not move, because "emitted nothing" and "resolved the transport and then put nothing on the wire" are the same observation. The case now clears and reads back first. This is the same F1 arming rule package L hit on tier 3.


Slice 4 — the vertex-layout split and vertex-format-conversion (§3.2)

What landed. BuildWireVertexInput answered eight conditions with one false, and the caller answered that with a P7-marked MagmaWireFatal — a session kill where the monolith arm masks the attribute out and declines the draw. Split by what the reason is ABOUT:

reason before after
buffer-window false → P7-marked Fatal false → named Fatal Magma:vertex-layout-buffer-window (no P7 marker: it is a permanent protocol error, not an unmigrated shape)
attribute-shape, native-fp64-format, format-map, native-format-feature, converted-format-feature, element-size, offset-overflow false → Fatal MGLOG_E_ONCE + unsupportedAttribMask |= 1<<loc + continue, then the monolith arm's own pre-flight in WireDraw.inc declines the draw when a masked bit is one the program reads
vertex-format-conversion P7-marked Fatal MGLOG_E_ONCE + return false (decline, rule I (a))

unsupportedAttribMask now enters the wire layout hash, for the reason the monolith entry's hash already carries it: two VAOs can otherwise produce identical bindings/attributes/divisors and differ only in which enabled attribute was masked, and a blind hash would serve one the other's pipeline. It was safe to omit only while the mask was always zero.

Count correction. CONTRACT-P7 §3.2 says "the other six". There are SEVEN non-buffer-window return sites in the tree: offset-overflow post-dates the audit's count and is an arithmetic guard rather than a shape one. It is masked with the rest — an attribute whose base offsets cannot be added is one Vulkan cannot fetch, the monolith arm never computes that sum at all, and masking is strictly safer than a Fatal for a number no application can reach on purpose.

Family-word question for the integrator. buffer-window now goes through MagmaWireFatal, whose family is UnmigratedVerb. The honest word for "the record's own numbers are impossible" is ProtocolCorruption. MG_Pipe::MGPipeFatalFamily carries only UnmigratedVerb and RoleViolation and its own comment says a third "costs a row here and an arm in the adapter - which is the point, because that arm is where the choice gets argued". Not taken here: it is a cross-package edit (MG_Pipe/PipeSessionFail.h + the MG_Remote adapter) and §3.3 records that the three Magma funnels keep UnmigratedVerb deliberately, because renaming invalidates the refusal census kept since P5b. Flagged rather than decided.

Census. grep -rn "@P7" MobileGL/ --include=*.cpp --include=*.inc --include=*.h: 15 sites / 4 files → 13 sites / 3 files. Renderer/WireDraw.inc leaves the list entirely (both of its marked refusals are retired). scripts/ci/fatal_census.py is unchanged at 79 abort sites over 20 files, 43 distinct family words, 3 refusal words, 0 unmarked — the funnels were already converted in wave 0, so retiring a MagmaWireFatal call moves no std::abort() count. (Note for anyone editing near this row: the census counts the marker literally, so a COMMENT that spells it raises the number. The first cut of this slice pushed 15 → 16 that way.)

New scenario. Scenarios/MagmaVertexLayoutScenario.cpp, DirectVulkan only — Espryt hands GL_FIXED to a driver that supports it, so the same case under the same name would assert a different fact there. DataType::Fixed32 has no arm in ToVkVertexFormat at all, which is what makes it the one unmappable shape reachable on host lavapipe; CONTRACT-P7 §2.5 records that no other P7-marked refusal is (they need real hardware), so without this shape the retirement would have had no host gate.

  • AnUnmappableVertexFormatDeclinesTheDrawInsteadOfEndingTheSession — the program READS the GL_FIXED array, so the draw declines and the framebuffer keeps the clear colour.
  • AnUnreadUnmappableArrayDoesNotStopTheRestOfTheDraw — the array is enabled and unmappable but unread, so the draw lands. This is what stops the first case being satisfied by "decline every draw that mentions GL_FIXED".

Registered on DirectVulkan.{Split,Spawn,Tcp}.VertexLayout. AND picked up by the ambient monolith DirectVulkan. discovery, so the A/B is direct: the same two assertions on the wire arm and on the monolith arm.

Proof (green). 8/8 — monolith DirectVulkan. ×2, Split ×2, Spawn ×2, Tcp ×2.

RED-ONCE (executed). format-map put back on the protocol verdict (return reject("format-map")):

70% tests passed, 3 tests failed out of 10
#4238 DirectVulkan.Split.VertexLayout...Unmappable...  Subprocess aborted***Exception
  MGPipe: Fatal{UnmigratedVerb, "Magma:vertex-layout-buffer-window"}
#4240 DirectVulkan.Spawn.VertexLayout...Unmappable...  Subprocess aborted
#4242 DirectVulkan.Tcp.VertexLayout...Unmappable...    Subprocess aborted
#3408 DirectVulkan.MagmaVertexLayoutScenario...Unmappable...   Passed   <- monolith control

The three split arms KILL THE PROCESS and the monolith arm stays green, which is exactly the divergence this slice closes. Restored: 10/10.

Lane arithmetic. The three new Split entries carry integration-magma-split, so that gating lane grows 73 → 75 (and -spawn 52 → 54, -tcp 54 → 56). Growth only; G14's name sets are grow-only by rule.


Slice 3 — OQ-8: one reflection archive serves the storage-block consumers (§5.3)

What landed.

  • LinkArtifacts gains Vector<StorageBlockReflection> storageBlocks ({name, binding, dataSize}), #if MOBILEGL_BUILD_DISAGGREGATED only — see G1 below.
  • ProgramLinkTask::SnapshotStorageBlockIndexSpace() fills it in phase A, right after SnapshotGlslangReflection.
  • Its VisitFields row and its own three-field table join the codec; kProgramArtifactsCodecVersion 2 → 3; ProgramArtifactsSchemaFingerprint() and therefore wireFingerprint move with them, which is expected and is what makes a mixed-version pair refuse at the handshake rather than at the first program.
  • MagmaProgramSource::EnsureStorageBlocks is deleted, with m_storageBlocks, m_storageBlocksReady, the local StorageBlock struct and #include <spirv_reflect.h>.
  • DirectVulkan.cpp's monolith consumer reads the archive too, under #if symmetry.

Why phase A and not phase B. The obvious home is where the SPIR-V exists, and there is exactly one function that sees both finished halves — ProgramSpirvTask::RunBody's L1 cache insert — but its writes reach only the TRANSLATION CACHE. Phase A's artifacts has already been moved into the ProgramObject by then (ProgramObject.cpp's JoinPendingLink), and phase B is structurally forbidden from touching it. A field filled there would therefore be present on a cache HIT and absent on every cold link — the worst of both, and invisible until a second identical program linked. So the fill is glslang-derived, in phase A, and the equivalence below is what makes that safe.

The equivalence is measured, not asserted. The consumers speak the index space SPIRV-Reflect produces, and the archive now produces it from glslang's reflection instead. ProgramTest.TheArchivesStorageBlockOrderIsTheOneSpirvReflectProduces runs the OLD algorithm — SPIRV-Reflect over the real modules, sorted by (normalised name, binding), deduplicated across stages — over a multi-stage program with an arrayed SSBO and an atomic-counter block, and compares element for element. Measured reference on this tree:

{ "Blocks", "VertexOnly", "gl_AtomicCounterBlock_0", "FragmentOnly" }

which exercises all three of the cases that could have diverged: the instance array collapses to one entry (glslang reflects per element, SPIR-V carries one descriptor with a count), the synthesized atomic-counter block IS in the backend's space although GL's own GL_SHADER_STORAGE_BLOCK enumeration excludes it, and FragmentOnly lands last because the cross-stage dedupe walks stages in linked order.

MEASUREMENT — spvReflectCreateShaderModule on the Magma wire draw path, per frame. Same binary, same workload, same window (frames=1 window=1 draws=1 acc=18, identical bytes), DirectVulkan × inproc, StorageBufferRegrowScenario + the pipeline storage-block case, with a temporary MGLOG_I("OQ8_REFLECT_CALL") at the call site:

reflect calls frames draws
BEFORE (HEAD's per-draw EnsureStorageBlocks) 4 1 1
AFTER (reads the archive) 0 1 1

The AFTER number is 0 structurally, not incidentally: the call site is gone and MagmaProgramSource.h no longer includes spirv_reflect.h at all, which is the gate a future edit would have to break on purpose. (A MagmaProgramSource is a borrowed view constructed once per wire draw, so its mutable memo was cold every time it was read — that is why the per-draw cost existed at all.)

The monolith half: DONE, under #if symmetry — with one caveat the integrator should see. The task allowed deferring this if it could not be done without moving pull .text. It can, and it is: GetShaderStorageBlockIndex / GetShaderStorageBlockBinding / ClearProgramResourceCaches and the block-binding setter each have a disaggregated arm that reads the archive and a #else arm that is the previous code verbatim. In the disaggregated build that retires g_programResourceCaches, ProgramResourceCache, StorageBlockResource, BufferVariableResource, AddBufferVariablesRecursive, NormalizeDescriptorName, GetProgramResourceCache and the rehash hazard — the open-addressed-map dangling reference that was a reproducible segfault in ProgramPipelineScenario's two storage-block cases.

Two things are worth stating plainly:

  1. The pull build keeps all of it. The archive member is disaggregated-only because one more Vector in LinkArtifacts changes its size, its implicit destructor and its move constructor, and G1 pins the pull build's .text byte for byte. So §5.3's "g_programResource Caches 随之删" is achieved in the disaggregated build and NOT in the pull one. Retiring the pull half is a one-line change (drop the guard on the member) the day that pin is retired or the member is moved — a P13 / integrator call, not a wave-2 one.
  2. Nothing outside DirectVulkan.cpp read any of the retired machinery. Measured: BufferVariableResource, bufferVariables, activeVariables and StorageBlockResource have no reference anywhere else in the tree, and dataSize was written and never read — GL_BUFFER_VARIABLE and GL_ACTIVE_VARIABLES are answered by MG_Impl/GLImpl/Program/ProgramInterface.cpp out of glslang's own reflection, which is a different index space. So the two arms differ in exactly the two functions their callers use and in nothing else.

One genuine behaviour improvement rides along: the monolith GetShaderStorageBlockBinding now asks GetShaderStorageBlockBindingOverride on every read, over an immutable list, where it used to read a binding patched into a mutable cache. Same answer by a shorter route, and a rebind can no longer be lost to a cache rebuild that raced a state-version bump.

Proof (green). ctest -L unit 2403/2403 (2402 before this slice; +1 is the new case). DirectVulkan.*(ProgramPipeline|StorageBufferRegrow) 60/60 — the monolith DirectVulkan. arm and the Split / Spawn / Tcp arms together, which is what says both consumers were switched.

G1 after the archive change: .text 0xa52203 = 10822147 → 10822147 (+0), 27837 → 27837 defined symbols, 0 added / 0 removed / 0 resized / 0 renamed, and the file is byte-identical on disk (19143824 bytes both sides). The #if guards did their job; an unguarded call site caught it once mid-slice (the pull build failed to compile, which is the loud version).

RED-ONCE (executed). SnapshotStorageBlockIndexSpace made to publish nothing:

ProgramTest.cpp:525: Failure
    Which is: {}
    Which is: { "Blocks", "VertexOnly", "gl_AtomicCounterBlock_0", "FragmentOnly" }
...
58% tests passed, 25 tests failed out of 60
  3091 DirectVulkan.ProgramPipelineScenario.ComputeAndGraphicsStagesShareOnePipeline  (SEGFAULT)
  3092 DirectVulkan.ProgramPipelineScenario.AStageProgramsStorageBlockBindingReaches... (Failed)
  3282 DirectVulkan.StorageBufferRegrowScenario...                                      (Failed)
  4179 DirectVulkan.Split.Buffers.StorageBufferRegrowScenario...                        (Failed)
  4198 DirectVulkan.Spawn.Buffers.StorageBufferRegrowScenario...                        (Failed)
  4217 DirectVulkan.Tcp.Buffers.StorageBufferRegrowScenario...                          (Failed)
  4469 DirectVulkan.Split.ProgramPipelineScenario...                                    (Failed)

The monolith arm reds beside the three transports, which is the evidence that the monolith consumer really was switched and not merely edited. Restored: 61/61.