Files
MobileGL/docs/Disaggregated/notes/recovered/wf3/scout-program-artifacts.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.

1. P6's plan and contract draft (neither started; not one line of code)

     docs/Disaggregated/P6-SPAWN-PLAN.md      the package plan
     docs/Disaggregated/P6-CONTRACT-DRAFT.md  the contract draft (moves to
                                              MG_Remote/CONTRACT-P6.md when c6 lands)

   The three findings that mattered, all written into the documents:

   a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
      the tree and already tested: SocketDoorbell including hangup detection and the two
      lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
      passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
      protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
      SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
      assembly plus a control plane plus a process, not "write a socket transport".

   b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
      audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
      the tree again. So the first package, a6, is a read-only audit that writes no code, and
      every contract row that depends on it is marked PENDING a6 rather than decided in
      advance.

   c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
      and protocol.fbs:186-196's own comment says Android transfers the window out of band -
      that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
      the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
      gate measures. A real window reaching the server is a named refusal. This gets its own
      section because "P6 is done" would otherwise read as "the game runs now".

   The contract adds rule G: a role may not name a process-local handle. It extends P5c's
   rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
   EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
   a token this contract defines or they do not cross, and a descriptor crosses only through
   SCM_RIGHTS, where the kernel renumbers it.

   What P5e left for P6 gets its own section. The latch semantics are already designed
   (ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
   client no longer waits while the peer process can DIE: the client holds records that were
   published and will never be applied, and Android's freezer suspends the child without
   closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
   That is P6's only new design; everything else is carriage. It shares a predicate with E1's
   outstanding re-specification (ID-122), and the two should be designed together.

   Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
   match integration-split's name for name, because a differing set means the lane is quietly
   running less; and the arm-proof gate requires every entry to record the child pid and
   transport=spawn in its OWN private log - ID-124 established that this kind of false green
   really happens, and a spawn lane that actually ran monolith would pass every other gate.
   S2's shape must be settled before it is written, and the document says why: E1 is the
   cautionary case.

2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)

   ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
   the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
   is under no version control. A record cited hundreds of times that lives only locally is not
   a record, so it moves in whole, with its directory structure intact.

   Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
   stay where they are: they are evidence DATA rather than documents - large, regenerated per
   run, and mostly carrying absolute paths and device serials that mean something only on the
   machine that produced them. The mapping and this split are written in
   docs/Disaggregated/notes/README.md.

   Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
   document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
   material salvaged from failed workflow runs, which may duplicate the like-named phase
   directories and is kept because a few of those were the only complete version at the time.

   CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
   deliberately outside it: those notes' file:line citations were written against their own
   code heads, and resolving them against today's HEAD would fail wholesale - which is not an
   error, it is their correct state as a historical record. To check one, use the revision it
   declares (--rev).

3. Inconsistencies fixed in passing

   - README.md's status line still said P5e was in progress while the stage table already said
     closed.
   - CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
     (a) and (b) were run in the previous commit; rewritten to closed plus the one item that
     actually remains (E1).
   - ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
     package order, the scope boundary and the two new gates.

Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
2026-09-18 23:31:39 -04:00

843 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scout report — `MG_State/GLState/ProgramState/ProgramArtifacts.h` (P0.5)
Worktree root (all paths below are relative to it unless absolute):
`C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`
Branch `feat/disaggregated`. Read-only scout; nothing was modified.
Every file:line below was opened and read.
---
## 0. The authoritative list of the five types
`docs/Disaggregated/ARCHITECTURE.md:260` (section 7, "Shader state = SPIR-V + 反射归档"), verbatim
naming of the five:
> P0.5 把 `TypeFacts`、`ResourceReflection`、`XfbVarying`、`LinkArtifacts`、`SpirvArtifacts` 抽到
> `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),更新 7 个
> includer,加 CI `-H` 闭包断言。
`docs/Disaggregated/ROADMAP.md:16` (P0.5 row) repeats it: "`ProgramArtifacts.h`(五个反射类型,不
include `ShaderObject.h`/`SpvcSession.h`,更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H`
include 闭包断言". Exit gate: "全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State`
include 能变红;`nm`/`.text` 变化可逐符号归因". Dependency line: "P0; **P1 与 P7 的硬前置**".
So the five are, in dependency order:
1. `TypeFacts`
2. `ResourceReflection` (+ its four aliases)
3. `XfbVarying`
4. `LinkArtifacts`
5. `SpirvArtifacts`
Other supporting facts from the docs:
- `ARCHITECTURE.md:258` — `CreateShaderState`'s payload is per-stage SPIR-V + the archive
(`LinkArtifacts` + `SpirvArtifacts`, **whole structs**), not source. glslang lives entirely on the
client, SPIRV-Cross (`TranspileSpirvToEssl`) entirely on the server, split at file granularity.
- `ARCHITECTURE.md:259` — archive mechanism is `Visit()` + a `sizeof` tripwire
(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`), one field table serving both
serialization directions. Required coverage listed explicitly: the four `ResourceReflection`
vectors (each carrying `TypeFacts`), `uniformSamplerOrImageUnitIndex`, `uniformBlockBinding`,
`shaderStorageBlockBinding` (by name), `explicitOpaqueUniformBindings`,
`xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture`, `computeLocalSize`, the
GS/TCS/TES facts, `usesReservedNumSamples`, `uniformOffsets`. `XfbVarying` carries two spellings
(GL name + block instance/member/element).
- `ARCHITECTURE.md:133` — `MGPProgramDesc` is 192 bytes: per-stage SPIR-V blob x6 + reflection blob
+ `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + four state bytes.
- `ARCHITECTURE.md:260` last sentence — without this step, P7's `nm -D | grep glslang` criterion is
unreachable.
- `ROADMAP.md` P7 row — gate is literally `nm -D libMobileGLServer.so | grep glslang` empty.
---
## 1. Current definitions — file:line, members, dependencies
All five live **inside `class ProgramObject`** in
`MobileGL/MG_State/GLState/ProgramState/ProgramObject.h` (1803 lines total; class opens at :25,
closes at :1802). They are therefore spelled `ProgramObject::TypeFacts` etc. everywhere.
### 1.1 `TypeFacts` — ProgramObject.h:44–72 (doc comment :41–43)
Flattened `glslang::TType`. All scalar POD; no heap member.
| line | member | type |
|---|---|---|
| 45 | `isArray` | `Bool` |
| 48 | `isSizedArray` | `Bool` |
| 49 | `isMatrix` | `Bool` |
| 50 | `isVector` | `Bool` |
| 51 | `isOpaque` | `Bool` |
| 52 | `isTexture` | `Bool` |
| 53 | `isImage` | `Bool` |
| 54 | `isDouble` | `Bool` (`getBasicType() == EbtDouble`) |
| 55 | `isVoid` | `Bool` (hidden block members) |
| 56 | `isBuffer` | `Bool` (`qualifier.storage == EvqBuffer`) |
| 57 | `isPatch` | `Bool` |
| 58 | `hasIndex` | `Bool` |
| 59 | `hasFormat` | `Bool` |
| 60 | `vectorSize` | `Int` |
| 61 | `matrixCols` | `Int` |
| 62 | `matrixRows` | `Int` |
| 63 | `layoutIndex` | `Int` |
| 64 | `layoutFormat` | `Uint` |
| 68 | `layoutMatrix` | `Int` (widened `glslang::TLayoutMatrix`) |
| 71 | `basicType` | `Int` (widened `glslang::TBasicType`) |
**Already glslang-free by type** — the widening to `Int`/`Uint` was done deliberately (comments at
:65–70 say so). `Bool`/`Int`/`Uint` come from `MG_Util/Types.h:33,29,30`.
### 1.2 `ResourceReflection` — ProgramObject.h:76–99 (doc comment :74–75); aliases :101–104
Flattened `glslang::TObjectReflection`; used for uniforms, blocks, pipe inputs and pipe outputs
alike.
| line | member | type |
|---|---|---|
| 77 | `name` | `String` (= `std::string`, `MG_Util/Types.h:21`) |
| 78 | `glDefineType` | `GLenum` |
| 79 | `offset` | `Int` = -1 |
| 82 | `size` | `Int` (raw `TObjectReflection::size`) |
| 86 | `index` | `Int` = -1 (TPROGRAM block index owning it) |
| 87 | `counterIndex` | `Int` = -1 |
| 88 | `arrayStride` | `Int` |
| 89 | `topLevelArraySize` | `Int` |
| 90 | `topLevelArrayStride` | `Int` |
| 91 | `binding` | `Int` = -1 |
| 92 | `location` | `Int` = -1 |
| 95 | `stages` | `Uint32` (an `EShLanguageMask`, widened) |
| 98 | `arraySize` | `GLint` = 1 |
| 99 (`type;`) | `type` | `TypeFacts` |
Aliases, ProgramObject.h:101–104:
```
using UniformReflection = ResourceReflection; // :101
using BlockReflection = ResourceReflection; // :102
using PipeInputReflection = ResourceReflection; // :103
using PipeOutputReflection = ResourceReflection; // :104
```
Dependencies: `String`, `GLenum`/`GLint` (GL headers via `Includes.h:68–73`), `TypeFacts`. **No
glslang type by value.**
### 1.3 `XfbVarying` — ProgramObject.h:1146–1171 (doc comment :1144–1145)
| line | member | type |
|---|---|---|
| 1147 | `name` | `String` (GL spelling, incl. `"Block.member"`) |
| 1148 | `type` | `GLenum` = `GL_FLOAT` |
| 1149 | `size` | `GLint` = 1 |
| 1150 | `bufferIndex` | `Uint32` |
| 1151 | `offsetBytes` | `Uint32` |
| 1152 | `byteSize` | `Uint32` |
| 1155 | `packedOffsetBytes` | `Uint32` |
| 1166 | `blockInstanceName` | `String` |
| 1167 | `blockName` | `String` |
| 1168 | `blockMemberIndex` | `Int` = -1 |
| 1171 | `blockMemberElement` | `Int` = -1 |
**No glslang type.** This is the "two spellings" struct `ARCHITECTURE.md:259` calls out.
### 1.4 `LinkArtifacts` — ProgramObject.h:1210–1393 (doc comment block :1173–1209)
53 members. Only **two** carry a glslang type:
- **:1215 `SharedPtr<glslang::TProgram> program;`** — live only between `LinkProgram()` and the end
of `DoReflection`; null for an L1-memo-served link; must not be dereferenced outside
`DoReflection` (comment :1211–1214).
- **:1282 `Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;`**
Full member list (line: declaration):
```
1215 SharedPtr<glslang::TProgram> program; <-- glslang
1218 Vector<UniformReflection> uniformReflection;
1219 Vector<BlockReflection> blockReflection;
1220 Vector<PipeInputReflection> pipeInputReflection;
1221 Vector<PipeOutputReflection> pipeOutputReflection;
1227 Bool lastStageIsFragment = false;
1228 Array<GLuint, 3> computeLocalSize{};
1231 UnorderedMap<String, Int> uniformIndexByName;
1234 Vector<String> attribs;
1235 Vector<GLenum> attribTypes;
1238 UnorderedMap<String, Uint> linkedFragDataLocation;
1239 UnorderedMap<String, Uint> linkedFragDataIndex;
1245 Vector<Int> glUniformIndexToTProgram;
1246 Vector<Int> tProgramUniformIndexToGl;
1247 Vector<Int> glBlockIndexToTProgram;
1248 Vector<Int> tProgramBlockIndexToGl;
1269 Vector<Int> glUniformBlockIndexToBlock;
1270 Vector<Int> blockIndexToGlUniformBlock;
1275 UnorderedMap<String, Int> linkedExplicitUniformLocations;
1282 Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues; <-- glslang
1283 UnorderedMap<String, Uint> uniformLocations;
1291 Vector<Uint64> writtenUniformLocationBits;
1292 Vector<Uint64> writtenUniformIndexBits;
1293 Vector<Uint> writtenUniformIndices;
1296 Vector<Int> uniformIndexInTProgram;
1298 Vector<Int> uniformSamplerOrImageUnitIndex;
1303 UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
1313 UnorderedMap<String, Uint> uniformBlockIndexByName;
1314 Vector<Int> uniformBlockBinding;
1325 UnorderedMap<String, Int> shaderStorageBlockBinding;
1331 std::set<String> storageBlocksWithoutBinding; <-- needs <set>
1336 std::set<String> uniformBlocksWithoutBinding; <-- needs <set>
1338 Uint activeUniformCount = 0;
1346 Bool usesReservedNumSamples = false;
1347 Uint maxUniformLocation = 0;
1348 Int uniformNameMaxLength = 0;
1349 Int attribInNameMaxLength = 0;
1350 Int uniformBlockNameMaxLength = 0;
1352 String infoLog;
1353 Bool linkStatus = false;
1357 Vector<XfbVarying> xfbVaryings;
1363 Vector<String> xfbInterfaceNames;
1364 Vector<Uint32> xfbStrides;
1365 Vector<Uint32> gsStripTriangles;
1366 Bool gsStripCaptureFixup = false;
1367 GLenum gsInputPrimitive = GL_NONE;
1371 Int tcsOutputVertices = 0;
1380 GLenum gsOutputPrimitive = GL_NONE;
1381 Int gsMaxVertices = 0;
1382 Int gsInvocations = 0;
1385 GLenum tessGenMode = GL_NONE;
1386 GLenum tessGenSpacing = GL_NONE;
1387 GLenum tessGenVertexOrder = GL_NONE;
1388 Bool tessGenPointMode = false;
1389 GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
1390 Int xfbVaryingNameMaxLength = 0;
1391 Bool xfbNeedsScatteredCapture = false;
1392 Uint32 xfbPackedStride = 0;
```
Membership rule stated at :1177–1180: "exactly the field list `ResetLinkArtifacts()` clears (plus
the four it forgot to — `infoLog`, `linkedFragDataLocation/Index` and the geometry strip-capture
pair)". Access rule (invariant I5) at :1187–1192: `m_artifacts` is private and reachable only via
`ProgramObject::Artifacts()`, which calls `EnsureLinkJoined()` first.
`std::set` is used at :1331 and :1336 but **`ProgramObject.h` never includes `<set>`** — it arrives
transitively (`Includes.h` includes `<map>` at :17 but not `<set>`; `<set>` appears explicitly only
in `MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h:15`, `ShaderCompiler.h:16`,
`ShaderSourceProcessor.h:10`, `SpirvPasses/FlattenXfbInterfaceBlocksPass.h:15`,
`SpirvPasses/UniquifyIoBlockNamesPass.h:16`, `TranslationCache.h:15`). **The new header must include
`<set>` itself** or it will fail to compile once the transitive path is cut.
### 1.5 `SpirvArtifacts` — ProgramObject.h:1409–1449 (doc comment :1395–1408)
| line | member | type |
|---|---|---|
| 1410 | `generatedSpirv` | `Vector<Vector<unsigned>>` |
| 1411 | `enableSpirvValidation` | `Bool` |
| 1414 | `uniformOffsets` | `Vector<Uint>` |
| 1415 | `globalUboScratch` | `Vector<Uint8>` |
| 1420 | `reservedNumSamplesOffset` | `Uint` = **`kInvalidUniformOffset`** |
| 1427 | `spirvStatus` | `Bool` |
| 1436 | `nativeFloat64` | `Bool` |
| 1448 | `pointSizeDemoted` | `Bool` |
**No glslang, no spirv-cross, no SPIRV-Reflect type.** One external dependency:
`kInvalidUniformOffset`, declared at **ProgramObject.h:547** as
`static constexpr Uint kInvalidUniformOffset = ~0u;` — i.e. a `ProgramObject` static member declared
*before* `SpirvArtifacts` but *outside* it. Moving `SpirvArtifacts` out requires moving (or
mirroring) this constant. Its readers, all of which spell it `ProgramObject::kInvalidUniformOffset`:
`MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:4659` and `:8553`;
`MG_Impl/GLImpl/Program/GL_Program.cpp:1249,1304,1522`; `MG_State/GLState/Core.cpp:513,514`;
`MG_State/GLState/ProgramState/ProgramObject.cpp:198,273`; plus in-header uses at
ProgramObject.h:565, :811, :1420.
### 1.6 `TUniformInitializer` — the one type that must be re-declared
`include/glslang/MachineIndependent/localintermediate.h:627–636` (vendored/forked glslang; this
struct is a MobileGL fork addition, see commit `e5846569c`):
```cpp
struct TUniformInitializer {
std::string name;
TBasicType basicType = EbtVoid;
int vectorSize = 1;
int matrixCols = 0;
int matrixRows = 0;
int arraySize = 1;
std::vector<long long> intValues;
std::vector<double> floatValues;
};
```
It is a plain aggregate — no pool allocator, no `TString`. `ProgramTranslationCache.h:44–47`
independently states this: "the only member that ever pointed into glslang-owned memory was
`program` itself, and `TUniformInitializer` / `XfbVarying`, which look like glslang types, are
`std::string` + `std::vector` aggregates."
Producers/consumers of `uniformInitialValues`:
- `MG_State/GLState/ProgramState/ProgramLinkTask.cpp:612–618` (dedupe + push_back)
- `MG_State/GLState/ProgramState/ProgramObject.cpp:148` (`ApplyUniformInitialValues`, reads it)
- `MG_State/GLState/ProgramState/ProgramObject.cpp:356` (`ResetLinkArtifacts` clears it)
**`ProgramObject.cpp:180–183` compares `init.basicType` against `glslang::EbtFloat`,
`glslang::EbtFloat16`, `glslang::EbtDouble`, `glslang::EbtInt`, `glslang::EbtUint`,
`glslang::EbtBool`.** Re-typing `basicType` to `Int` keeps this compiling only because those
enumerators implicitly convert; if the new type is to be glslang-free, that `.cpp` (which is
client-side and may keep including glslang) stays as-is, but the *header* must not name
`glslang::TBasicType`.
---
## 2. Transitive include closure of `ProgramObject.h` today
Direct includes, `ProgramObject.h:10–14`:
```
:10 #include <Includes.h>
:11 #include "ShaderObject.h"
:13 #include <MG_Util/Metrics/BufferMetrics.h>
:14 #include <MG_Util/ShaderTranspiler/SpvcSession.h>
```
Level 2:
- `MG_State/GLState/ProgramState/ShaderObject.h:10–13` →
`<Includes.h>`, `ShaderStage.h` (no includes at all), `ShaderCompileTask.h`,
`ShaderCompileAdoptionMap.h`.
- `MG_Util/ShaderTranspiler/SpvcSession.h:10–12` → `<Includes.h>`, **`<spirv_reflect.h>`**,
`MG_Util/ShaderTranspiler/Types.h`.
- `MG_Util/Metrics/BufferMetrics.h:10` → `<Includes.h>` only.
Level 3:
- `ShaderCompileTask.h:10–15` → `<Includes.h>`, `MG_Util/Async/JobNode.h`,
`MG_Util/ShaderTranspiler/CompileEnv.h`, `MG_Util/ShaderTranspiler/Types.h`,
**`MG_State/GLState/BufferState/BufferState.h`**, `ProgramState/ShaderPreprocessCache.h`.
- `ShaderCompileAdoptionMap.h:10–11` → `<Includes.h>`, `ProgramState/ShaderSourceKey.h`.
- `MG_Util/ShaderTranspiler/Types.h:10–11` → `<Includes.h>`, `CompileEnv.h`.
**`Includes.h` is the elephant.** `MobileGL/Includes.h:79–83`, unconditional:
```
#include <glslang/Include/Types.h>
#include <glslang/Public/ShaderLang.h>
#include <glslang/SPIRV/GlslangToSpv.h>
#include <glslang/Include/intermediate.h>
#include <glslang/MachineIndependent/localintermediate.h>
```
plus `Includes.h:56` `<spirv_cross/spirv_cross_c.h>`, `:53` `<ska/flat_hash_map.hpp>`, `:55`
`<xxhash.h>`, `:63–74` EGL/GL/GLES headers, `:130` `<vulkan/vulkan.h>`, `:148` `MG_Util/Debug/Log.h`,
`:149` `MG_Util/Types.h` (which at :11 includes `<Includes.h>` back and at :12 `GLExtensions.h`).
**Consequence to flag loudly for the implementers:** the ARCHITECTURE wording "只 include
`<Includes.h>` 与容器" does **not** by itself remove glslang from the preprocessed text of
`ProgramArtifacts.h`, because `Includes.h` drags glslang in unconditionally. What it removes is the
*by-value/by-symbol* dependency (`SharedPtr<glslang::TProgram>` needs `~TProgram`;
`Vector<TUniformInitializer>` needs the type complete) — which is exactly what the P7 gate
(`nm -D | grep glslang`, a **link/symbol** gate) measures. Two options for the header, both worth
recording in the design note:
- (a) include `<Includes.h>` as the docs say and rely on the type-level purge; the `-H` closure
assertion then asserts the absence of `ShaderObject.h` / `SpvcSession.h` / `spirv_reflect.h`, not
the absence of glslang.
- (b) include only `MG_Util/Types.h` + `<set>` + GL headers, which would be genuinely glslang-free —
but `MG_Util/Types.h:11` itself includes `<Includes.h>`, so this is not achievable without
splitting `Types.h` too. **(a) is the only reachable shape today**; say so in the header comment.
Where `Includes.h` gets its type aliases (`MG_Util/Types.h`): `String` :21, `Int8..Uint64` :23–32,
`Bool` :33, `Float`/`Double` :34–35, `StringView` :36, `Vector` :37–38, `Pair` :39–40, `SharedPtr`
:41–42, `UniquePtr` :43–44, `WeakPtr` :45–46, `SizeT` :55, `Array` :56–57,
`UnorderedMap = ska::flat_hash_map` :87–88, `Optional` :117, `Data = Vector<Uint8>` :123.
---
## 3. Every includer — verified counts
### 3.1 `ProgramObject.h` — 8 non-self includers (9 with its own .cpp)
| # | file:line | what it uses |
|---|---|---|
| 1 | `MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h:13` | `ProgramObject` by reference in `ComputeHash` (:474), `GetOrCreateProgram` (:475), `VkProgramObject::program` pointer (:583), `stages` doc note (:590). Names **no** artifact type. |
| 2 | `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:13` | `const ProgramObject&` parameter on ~15 resolvers (:232, :250, :465, :751, :808, :835, :862, :986, :1146, :1260, :1643, :1712, :1759, :1802, :1884). Accessors only; names no artifact type. |
| 3 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:17` | `ProgramObject::kInvalidUniformOffset` (:4659, :8553); constructs internal blit/depth-mipmap `ProgramObject`s (:4232–4253, :4312–4316). |
| 4 | `MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp:11` | **the heaviest consumer.** `ProgramObject::TypeFacts` (:84, :187, :198, :435), `ProgramObject::ResourceReflection` (:93), `ProgramObject::BlockReflection` (:176, :251), `ProgramObject::LinkArtifacts` (:228, :265, :328, :437, :528). Fields read off `LinkArtifacts`: `uniformReflection`, `blockReflection`, `pipeInputReflection`, `pipeOutputReflection`, `lastStageIsFragment` (only those five). |
| 5 | `MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h:11` | `ProgramObject::LinkArtifacts artifacts;` (:80) and `SpirvHandoff::reflection` (:118); `SharedPtr<const ProgramObject::SpirvArtifacts> cachedSpirv` (:147); `SharedPtr<const ProgramObject::LinkArtifacts> linkArtifactsForCache` (:151). Also holds `Vector<SharedPtr<glslang::TShader>> shaders` (:108) — genuinely glslang, stays client-side. |
| 6 | `MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h:11` | pipeline composite; holds `SharedPtr<ProgramObject>` per stage. |
| 7 | `MobileGL/MG_State/GLState/ProgramState/ProgramState.h:12` | the object table. **This is the fan-out node**: `MG_State/GLState/Core.h:16` includes it, so every `Core.h` includer (all of `MG_Impl`, both backends' `Managers.h`/`DirectVulkan.cpp`) reaches `ProgramObject.h` transitively. |
| 8 | `MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.h:11` | `ProgramTranslationResult { ProgramObject::LinkArtifacts link; ProgramObject::SpirvArtifacts spirv; }` (:50–51). |
| (self) | `MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp:9` | — |
**The roadmap's "7 个 includer" does not reproduce as the ProgramObject.h includer count (8 non-self,
9 with the .cpp).** It *does* reproduce exactly as the `ShaderObject.h` includer count (7, below), so
the number is either a miscount or was written against `ShaderObject.h`. Implementers should plan
for **8 includers of `ProgramObject.h`** plus the files that only *name* the types (§3.4).
### 3.2 `ShaderObject.h` — exactly 7 includers
| # | file:line | uses |
|---|---|---|
| 1 | `MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h:14` | `ShaderObject` for stage/source |
| 2 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:18` | builds internal shaders (:4232 ff.) |
| 3 | `MobileGL/MG_State/GLState/ProgramState/ProgramObject.h:11` | `SharedPtr<ShaderObject>` attach lists, `LinkedShaderRef` (:150–154) |
| 4 | `MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp:9` | self |
| 5 | `MobileGL/MG_Util/Converters/GLToMG/ProgramEnumConverter.h:11` | `ShaderStage` conversion |
| 6 | `MobileGL/MG_Util/Converters/MGToGL/ProgramEnumConverter.h:11` | ditto |
| 7 | `MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h:13` | source preprocessing |
### 3.3 `SpvcSession.h` — 18 includers
`MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp:13`;
`MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp:60`;
`MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp:43`;
`MobileGL/MG_State/GLState/ProgramState/ProgramObject.h:14`;
`MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp:14`;
`MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp:18`;
`.../DemotePointSizeTest.cpp:17`; `.../FlattenXfbInterfaceBlocksTest.cpp:18`;
`.../LegalizeResourceArrayIndexTest.cpp:17`; `.../LowerViewportIndexTest.cpp:29`;
`.../StripIoBlockLocationsTest.cpp:17`; `.../TranslationCacheTest.cpp:39`;
`.../UniquifyIoBlockNamesTest.cpp:19`; `.../WidenImageFormatsTest.cpp:34`;
`MobileGL/MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.cpp:10`;
`MobileGL/MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h:11`;
`MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h:11`;
`MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp:9`.
**`ProgramObject.h:14` is the only one of the 18 that is a `ProgramObject.h` include** — dropping it
costs nothing to the other 17. Grep confirms `ProgramObject.h` names no `spvc_*` / `SpvReflect*` /
`SpvcMetadata` symbol in its own body; the include is dead weight there (verify with a build before
deleting — `ProgramObject.cpp` may rely on it transitively).
There are **no includers of any of the three headers outside `MobileGL/`** (checked the whole
worktree; `Testing/`, `tools/`, `3rdparty/` have none).
### 3.4 Files that *name* one of the five types (these need `ProgramArtifacts.h`, or keep getting it
via `ProgramObject.h`)
- `MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp:27–28` (`using TypeFactsRef = const ...::ProgramObject::TypeFacts&`), then :243, :250, :1197, :1213, :1246, :1301, :1545.
- `MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp` — see §3.1 row 4.
- `MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp:48–49` (`MakeTypeFacts`), :79–81 (`MakeResourceReflection`), :94, :133, :866, :872, :876–877, :987, :991, :1114, :1122, :1302, :1331, :1383, :1439, :1539, :1730, :1758, :1768, :1789, :1796, :1857, :1944, :1960, :2038.
- `MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h:80, :114, :118, :145, :147, :148, :151, :203`.
- `MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h:46`; `ProgramSpirvTask.cpp:127, :196, :234, :349`.
- `MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp:198, :207, :273, :332–343, :356, :499–501, :686`.
- `MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.h:21, :34, :38–39, :46, :50–51`; `ProgramTranslationCache.cpp:39–40` (`sizeof(ProgramObject::ResourceReflection)`).
- `MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h:54, :429, :442` — **comments only**; MG_Util deliberately must not depend on MG_State (`ProgramTranslationCache.h:38–39`).
- `MobileGL/MG_Pipe/MGPipeTypes.h:296` — comment on `MGPProgramDesc` naming the archive.
- `MobileGL/MG_Test/Program/AsyncSpirvPhaseTest.cpp:336` — comment.
- `MobileGL/MG_Backend/DirectGLES/Managers.cpp:6424, :6440` — uses `TypeFacts` through `const auto&`, never spelling the name.
---
## 4. How the two backends read the artifacts today
**Neither backend names `LinkArtifacts`, `SpirvArtifacts`, `ResourceReflection` or `XfbVarying`
anywhere.** Both go exclusively through `ProgramObject` accessors, which are what the `PipeInputs`
work of P1 replaces. This is good news for P0.5: moving the type declarations does not touch backend
code at all.
### 4.1 DirectGLES (Espryt) — reads the **front-end tables**
Reaches `ProgramObject.h` transitively: `DirectGLES/Managers.h:17` → `MG_State/GLState/Core.h:16` →
`ProgramState/ProgramState.h:12` → `ProgramObject.h`. (`DirectGLES.h:10–15` and `Managers.h:10–18`
contain no direct include.)
Call sites (file:line → accessor → underlying artifact field):
*XFB (all `LinkArtifacts`)*
- `DirectGLES.cpp:890` `GetTransformFeedbackPackedStride()` → `xfbPackedStride`
- `DirectGLES.cpp:925, :1034` `GetTransformFeedbackStride(i)` → `xfbStrides[i]`
- `DirectGLES.cpp:931`, `Managers.cpp:7448`, `Managers.cpp:8092` `GetTransformFeedbackVaryings()` → `xfbVaryings`
- `DirectGLES.cpp:1005` `GetTransformFeedbackBufferCount()`
- `DirectGLES.cpp:1027` `NeedsScatteredTransformFeedbackCapture()` → `xfbNeedsScatteredCapture`
- `DirectGLES.cpp:1039, :1063` packed stride again (scatter buffer bind)
- `DirectGLES.cpp:1097`, `Managers.cpp:8140, :8155, :8219` `GetTransformFeedbackBufferMode()` → `xfbBufferMode`
- `Managers.cpp:8090` `GetTransformFeedbackVaryingCount()`
*Attributes / uniforms*
- `DirectGLES.cpp:1247` `GetActiveAttributeLocationMask()`
- `DirectGLES.cpp:1276, :1290` `GetAttribType(location)` → `attribTypes`
- `DirectGLES.cpp:3036`, `Managers.cpp:6419, :6572` `GetMaxUniformLocation()` → `maxUniformLocation`
- `DirectGLES.cpp:3038, :3564`, `Managers.cpp:468, :6450, :6589` `GetUniformSamplerOrImageUnitIndex(loc)` → `uniformSamplerOrImageUnitIndex`
- `DirectGLES.cpp:3039`, `Managers.cpp:6423, :6576` `GetUniformType(loc)`
- `Managers.cpp:463, :465, :6579` `GetUniformLocation(name)` → `uniformLocations`
- `Managers.cpp:6421, :6574` `GetUniformName(loc)` → `uniformReflection[...].name`
- **`Managers.cpp:6424` `GetUniformTypeFacts(loc)` → `uniformReflection[...].type` — the only direct
`TypeFacts` read in either backend** (comment at :6440: "From the OWNED TypeFacts, not from a live
TType"). It is bound with `const auto&`, so the type name never appears.
*Blocks / SPIR-V / status*
- `DirectGLES.cpp:2734, :3033, :3336, :3374, :3663, :4071`, `Managers.cpp:7321` `GetLinkStatus()` / `GetSpirvStatus()` → `linkStatus` / `spirvStatus`
- `DirectGLES.cpp:3397, :3402, :3444, :3449`, `Managers.cpp:8290, :8293` `GetUBOSize()` → `globalUboScratch.size()`
- `DirectGLES.cpp:3481` `GetUniformBlockBinding(i)` → `uniformBlockBinding`
- `Managers.cpp:6192, :6202, :7341` `GetShaderStorageBlockBindingOverrides()` → `shaderStorageBlockBinding`
- `Managers.cpp:7408` `GetLinkedShaderStages()`; `:7409` `GetGeneratedSpirv()` → `generatedSpirv`;
`:7430` `GetLinkedShaderSnapshot()`; `:7439` `GetSpirvValidationEnabled()` → `enableSpirvValidation`
- version/identity: `GetLinkVersion` (`DirectGLES.cpp:2798, :3715, :7368`), `GetImageUnitVersion`
(:2799), `GetBackendStateVersion` (:3335, :3350, :3540), `GetLifetimeId` (:3333, :3349),
`GetExternalIndex` (many, `Managers.cpp:7212` ff.). These live on `ProgramObject`, **not** in the
artifacts — they must stay behind.
`ARCHITECTURE.md:310` additionally notes Espryt's `ScatterCapturedRecords` is a read-modify-write on
the *client* shadow and that under split the client keeps the varying/stride tables from the
reflection archive.
### 4.2 DirectVulkan (Magma) — re-derives with **SPIRV-Reflect**
`MG_Backend/DirectVulkan/DirectVulkan.cpp:22` `#include <spirv_reflect.h>`.
The re-derivation lives in `GetProgramResourceCache(const ProgramObject&)` at
**`DirectVulkan.cpp:161–2xx`** (the docs' "around line 161" is exact — the function opens at :161,
the cache lookup at :162). Shape:
- Caches keyed by GL program name: `UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;`
at `DirectVulkan.cpp:103`; cleared at `:305` (`ClearProgramResourceCaches`); insertion hazard note
at `:728`.
- `struct BufferVariableResource` `:51–57`, `struct StorageBlockResource` `:58–63`,
`struct ProgramResourceCache` `:65–81` (`programLifetimeId`, `backendStateVersion`,
`blockBindingVersion`, `Vector<StorageBlockResource> storageBlocks`,
`Vector<BufferVariableResource> bufferVariables`).
- Validity key `:171–174`: `GetLifetimeId()` + `GetBackendStateVersion()` +
`GetBlockBindingVersion()`. On a block-binding-only bump (`:176–190`) it re-applies
`program.GetShaderStorageBlockBindingOverride(block.name)` **by name** instead of re-reflecting —
i.e. `LinkArtifacts::shaderStorageBlockBinding` is the authority and spirv-reflect is the
structure.
- The reflect run itself `:196–...`: `program.GetGeneratedSpirv()` → per module
`spvReflectCreateShaderModule(spirv.size() * sizeof(Uint), spirv.data(), &module)` (:205),
`spvReflectEnumerateDescriptorBindings` (:216, :222).
- `NormalizeDescriptorName` `:118–131` strips a `"[0]"` suffix off `type_description->type_name`.
- `AddBufferVariablesRecursive` `:133–159` walks `SpvReflectBlockVariable::members` recursively to
build the `GL_BUFFER_VARIABLE` list with names/offsets/sizes.
This is exactly the "three consumers of one archive" open question at `ROADMAP.md:81`: Espryt reads
the front-end tables, Magma runs SPIRV-Reflect, and `DirectVulkan.cpp` reflects a second time for
`glGetProgramResource*`. **P0.5 changes none of it** — but note that the archive as shipped
(`LinkArtifacts` alone) does not contain buffer-variable / storage-block reflection, which is why
Magma re-derives; the server can keep doing exactly that from the shipped SPIR-V.
Magma's other `ProgramObject` reads (`UniformManager.cpp`, `ProgramFactory.h/.cpp`,
`VulkanRenderer.cpp`) are all accessor calls; see §3.1 rows 1–3.
---
## 5. Existing `Visit()` / archive / serialization patterns to follow
**There is no `Visit()`, `Archive()`, `Serialize()` or `ForEachField()` in the tree today**
(`grep -rn "Visit(\|ForEachField\|Archive(\|Serialize(\|Deserialize("` over `MobileGL/` returns
nothing). The `Visit()` archive is genuinely new. The two prior-art patterns to imitate:
### 5.1 The X-macro field table — `MG_Pipe/PipeFields.def`
`MobileGL/MG_Pipe/PipeFields.def:9–18` states the contract: one macro per payload in
`MGPipeTypes.h`, listing only the fields that carry **meaning** (padding deliberately absent, because
`MOBILEGL_PIPE_VERIFY` must have zero false positives). Hand-maintained alongside the struct; a field
added without a `.def` entry makes the comparator blind to it.
Shape (`PipeFields.def:83–85`):
```c
#define MGP_FIELDS_MGPProgramDesc(F) \
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
```
and a roll-up list at `PipeFields.def:227` enumerating every payload. Consumed by
`scripts/gen_pipe.py` (docstring at `scripts/gen_pipe.py:9–33`), which emits
`MobileGL/MG_Pipe/generated/{PipeWire,PipeVerify,PipeTables,PipeThunks,PipeFilled,PipeSpanTable,PipeCoverage}.inc`.
Outputs are committed; CI regenerates and fails on a diff. `gen_pipe.py` refuses a catalogue payload
with no field list.
**Recommendation:** the `Visit()` archive is the C++-template analogue of this — but because
`LinkArtifacts` members are `String`/`Vector`/`UnorderedMap`/`std::set` rather than scalars, a
generated `.inc` is less natural than a member-template. The prior art to cite is the
*discipline* (one field table, both directions, a gate that goes red on a missed field), not the
macro mechanics.
### 5.2 The sizeof tripwire — `MG_Pipe/MGPipeTypes.h`
Two idioms, both worth copying:
**(a) The POD assertion macro**, `MGPipeTypes.h:43–45`:
```c
#define MGP_ASSERT_POD(T, Size) \
static_assert(std::is_trivially_copyable_v<T>, #T " must be trivially copyable"); \
static_assert(sizeof(T) == (Size), #T " changed size; update the wire format and this assertion")
```
used e.g. `MGP_ASSERT_POD(MGPProgramDesc, 192);` at `MGPipeTypes.h:308`. Rationale at
`MGPipeTypes.h:20–22`: "Sizes are asserted rather than merely documented because the wire records
generated from these structs are memcpy'd; a field silently changing width is a protocol break that
no test would otherwise see."
**(b) The named-constant ratchet**, `MGPipeTypes.h:527–539` — this is the exact shape
`MGL_LINKARTIFACTS_SIZE` should take:
```c
#define MGL_RESIDUAL_BLOCK_SIZE 1248
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
"retired, and do not raise it");
```
with a comment block (:527–535) explaining the direction of travel and what makes it a build break.
`MGPipeTypes.h:504–507` adds the caveat that a heterogeneous POD **also** needs member-by-member
`offsetof` assertions, since padding differs across ABIs and the monolith verify harness is blind to
it (both sides are one TU).
**Caveat for `LinkArtifacts`:** `sizeof(LinkArtifacts)` is **not ABI-stable and not
trivially-copyable** — it contains `std::string`, `std::vector`, `ska::flat_hash_map` and `std::set`,
whose sizes differ between libstdc++/libc++/MSVC and between debug/release STL configurations.
`MGL_LINKARTIFACTS_SIZE` therefore cannot be a single literal the way `MGL_RESIDUAL_BLOCK_SIZE` is;
it is a **same-build tripwire** ("did somebody add a field without touching `Visit()`"), not a wire
size. Options: (i) assert per-toolchain values behind `#if`, (ii) assert only under a canonical CI
toolchain (`#if defined(__GLIBCXX__) && !defined(_GLIBCXX_DEBUG)`), or (iii) drop the byte count and
assert a **field count** the `Visit()` template computes at compile time. State whichever you pick in
the header — the ARCHITECTURE wording assumes (i)/(ii).
### 5.3 The `-H` include-closure gate
`ARCHITECTURE.md:425` describes purity gate A: `MG_Remote/Transport/` headers must not include the
front-end umbrella, "纯度门 A 断言 `-H` 输出". **No such script exists yet** — `scripts/` contains
only `check_doc_citations.py`, `format_code.sh`, `gen_pipe.py`, `gen_pipe_dirty_surface.py`,
`gen_protocol.py`, `update-source-file-header.py`. The P0.5 implementer writes the first one, and
`ROADMAP.md:16` requires it be demonstrably red when a `MG_State` include is added back.
Related counter-example already in the tree: `MGPipeTypes.h:24–31` documents its own P0.5 debt in the
file header ("P0.5 DEBT, recorded here so it is impossible to miss") for `#include
<MG_Backend/BackendObject.h>` (:32) and `#include <MG_State/GLState/RenderState/RenderState.h>`
(:33). **Follow that convention**: any include `ProgramArtifacts.h` cannot yet drop gets an explicit
debt paragraph in the file header.
---
## 6. Proposed contents of `ProgramArtifacts.h`
File: `MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h`
Namespace: `MobileGL::MG_State::GLState` (**top-level, not nested in `ProgramObject`**).
### 6.1 Includes
```cpp
#pragma once
#include <Includes.h> // String/Vector/UnorderedMap/Array + GL enums; see debt note below
#include <set> // std::set<String>, used by LinkArtifacts - NOT in Includes.h
```
Nothing else. Explicitly **not**: `ShaderObject.h`, `SpvcSession.h`, `ProgramLinkTask.h`,
`BufferMetrics.h`, `<spirv_reflect.h>`.
File-header debt paragraph (mandatory, mirroring `MGPipeTypes.h:24–31`): `<Includes.h>` still pulls
glslang (`Includes.h:79–83`), spirv-cross C (`:56`) and Vulkan (`:130`). This header is glslang-free
**by type and by symbol**, which is what the P7 `nm -D | grep glslang` gate measures; a textually
glslang-free closure additionally requires splitting `MG_Util/Types.h` (which itself includes
`<Includes.h>` at :11) and is out of P0.5 scope.
### 6.2 Types, in dependency order
1. **`kInvalidUniformOffset`** — move `static constexpr Uint kInvalidUniformOffset = ~0u;`
(ProgramObject.h:547) out to a namespace-scope `inline constexpr Uint kInvalidUniformOffset = ~0u;`
in this header, and leave `static constexpr Uint kInvalidUniformOffset = kInvalidUniformOffset;`…
no — leave a **`using`-style re-export** in `ProgramObject`:
`static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;`
so the 9 existing spellings `ProgramObject::kInvalidUniformOffset` (§1.5) keep compiling
unchanged. This is required: `SpirvArtifacts::reservedNumSamplesOffset` (:1420) uses it as a
default member initializer.
2. **`struct TypeFacts`** — verbatim move of ProgramObject.h:44–72, comments included. Zero changes;
already POD, already glslang-free.
3. **`struct ResourceReflection`** — verbatim move of :76–99, plus the four aliases :101–104 moved to
namespace scope.
4. **`struct XfbVarying`** — verbatim move of :1146–1171. Zero changes.
5. **`struct UniformInitializer`** — **NEW**, the de-glslang'd replacement for
`glslang::TIntermediate::TUniformInitializer` (`include/glslang/MachineIndependent/localintermediate.h:627–636`):
```cpp
struct UniformInitializer {
String name;
Int basicType = 0; // widened glslang::TBasicType, same convention as TypeFacts::basicType
Int vectorSize = 1;
Int matrixCols = 0;
Int matrixRows = 0;
Int arraySize = 1;
Vector<Int64> intValues; // was std::vector<long long>
Vector<Double> floatValues; // was std::vector<double>
};
```
`Int64` = `MG_Util/Types.h:31`, `Double` = `:35`. The widening of `basicType` to `Int` matches the
precedent already set by `TypeFacts::basicType` (ProgramObject.h:71) and
`TypeFacts::layoutMatrix` (:68).
6. **`struct LinkArtifacts`** — moved from :1210–1393, with **exactly two member re-types**:
- `:1215 SharedPtr<glslang::TProgram> program;` → **stays in `ProgramObject.h`**, see §6.3.
- `:1282 Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;` →
`Vector<UniformInitializer> uniformInitialValues;`
All other 51 members move verbatim.
7. **`struct SpirvArtifacts`** — verbatim move of :1409–1449, once `kInvalidUniformOffset` is in
scope (item 1).
8. **The archive** — `Visit()` as a member template on each of the five, plus the tripwires:
```cpp
template <class V> void TypeFacts::Visit(V&& v) { v(isArray); v(isSizedArray); /* 20 fields */ }
```
with a matching `static_assert` per struct. The `Visit()` must be `const`-overloaded (or take
`auto&& self`, C++23 deducing-this — the project is C++23, see `Includes.h:47`
`__cplusplus >= 202302L`) so one field list serves both write (const) and read (mutable), which is
the "一份字段表服务序列化两个方向" requirement of `ARCHITECTURE.md:259`.
### 6.3 What must **stay** in `ProgramObject.h`
- `ProgramObject::MAX_UNIFORM_LOCATIONS` (:39) — `static_cast<Int>(glslang::TQualifier::layoutLocationEnd)`.
Named by `MG_Impl/GLImpl/Getter/GL_Getter.cpp:1945`, `ProgramLinkTask.cpp:1251`. Stays glslang-bound;
it is not one of the five types. (If P7 later needs it server-side, replace the RHS with the literal
and `static_assert` against glslang on the client — flag but do not do this in P0.5.)
- **`LinkArtifacts::program`** — the `SharedPtr<glslang::TProgram>`. Two options; pick (A):
- **(A) Keep it inside `LinkArtifacts` but change nothing about the header's purity by making the
field's type a forward-declared incomplete `glslang::TProgram`.** `SharedPtr<T>` of an incomplete
`T` is legal to declare, copy and move; only construction and destruction need completeness, and
both happen in `ProgramLinkTask.cpp` (the only writer: :646, :678, :764, :846, :877) and
`ProgramObject.cpp:499–501` (whole-struct reset). **But** `LinkArtifacts`'s own implicit
destructor must destroy the `shared_ptr`, and that is instantiated wherever a `LinkArtifacts` is
destroyed — including in `ProgramInterface.cpp` and `ProgramTranslationCache.cpp`. A
`shared_ptr`'s destructor does **not** require `T` complete (the deleter was type-erased at
construction), so this works: `namespace glslang { class TProgram; }` at the top of
`ProgramArtifacts.h` is sufficient. **Verify by compiling with a hand-edited `Includes.h` that
hides glslang**, because this is the single highest-risk claim in this plan.
- (B) Move `program` out of `LinkArtifacts` onto `ProgramLinkTask` and pass it beside the
artifacts. Cleaner for the server, but touches ~30 call sites in `ProgramLinkTask.cpp`
(§1.4 readers) and `ProgramSpirvTask.cpp:127, :234` — a bigger diff than "纯搬移" allows.
Record as the P7 follow-up if (A)'s incomplete-type trick fails on any toolchain.
- Everything else: the ~120 accessors, `Artifacts()`/`Spirv()` join gates (:1682–1698),
`EnsureLinkJoined`/`EnsureSpirvJoined` (:1633, :1648), `ResetLinkArtifacts` declaration (:1457,
defined `ProgramObject.cpp:332`), `LinkedShaderRef` (:150–154), `PendingUniformWrite` (:1658–1663),
`BackendHashMemoSlot` (:1767–1771), and all `m_*` members (:1712–1801).
### 6.4 Forwarding shape
`ProgramObject.h` keeps compiling every existing spelling (`ProgramObject::LinkArtifacts`,
`ProgramObject::TypeFacts`, …) via in-class `using` re-exports, so **no other file changes at all**
in P0.5 (this is what makes the exit gate "全套测试逐名不变(纯搬移)" achievable):
```cpp
// ProgramObject.h, replacing lines 41-104, 1144-1449
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
...
class ProgramObject {
public:
using TypeFacts = MobileGL::MG_State::GLState::TypeFacts;
using ResourceReflection = MobileGL::MG_State::GLState::ResourceReflection;
using UniformReflection = ResourceReflection;
using BlockReflection = ResourceReflection;
using PipeInputReflection = ResourceReflection;
using PipeOutputReflection = ResourceReflection;
using XfbVarying = MobileGL::MG_State::GLState::XfbVarying;
using UniformInitializer = MobileGL::MG_State::GLState::UniformInitializer;
using LinkArtifacts = MobileGL::MG_State::GLState::LinkArtifacts;
using SpirvArtifacts = MobileGL::MG_State::GLState::SpirvArtifacts;
static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;
...
```
`ProgramObject.h` also **drops `#include <MG_Util/ShaderTranspiler/SpvcSession.h>` (:14)** — it names
no spvc/SpvReflect symbol (verified by grep). Keep `#include "ShaderObject.h"` (:11) and
`#include <MG_Util/Metrics/BufferMetrics.h>` (:13); both are genuinely used by `ProgramObject`
itself. Removing :14 is the one purity win available to `ProgramObject.h` in this phase; the glslang
dependency stays because `MAX_UNIFORM_LOCATIONS` and `SharedPtr<glslang::TProgram>` are there.
Note the alias `using TypeFacts = ...::TypeFacts;` inside a class whose enclosing namespace already
declares `TypeFacts` is legal (member alias shadows), but the qualified RHS is mandatory or it
self-references. Spell it fully qualified as above.
### 6.5 Where the `Visit()` archive's two directions live
Serializer/deserializer bodies do **not** belong in `ProgramArtifacts.h` (that would drag whichever
byte-stream helper into every `Core.h` consumer). Put them where `MGPProgramDesc::Reflection`'s blob
is built and parsed — the natural home is a new `MG_Pipe/` or `MG_Remote/` TU that includes
`ProgramArtifacts.h` and nothing else from `MG_State`. `ProgramArtifacts.h` carries only the field
tables (`Visit`) and the tripwires.
---
## 7. Risk list
**R1 — `Includes.h` is not glslang-free (HIGH, design-level).** `Includes.h:79–83` pulls all five
glslang headers unconditionally. Following the ARCHITECTURE literally ("只 include `<Includes.h>` 与
容器") yields a header that is glslang-free *by type* but not *by preprocessed text*. Decide and
document which gate the `-H` assertion enforces before writing it, or the gate will be written to a
criterion the header cannot meet. See §2.
**R2 — `sizeof(LinkArtifacts)` is not a stable number.** `String`/`Vector`/`ska::flat_hash_map`/
`std::set` sizes vary by STL and by `_GLIBCXX_DEBUG`. `MGL_LINKARTIFACTS_SIZE` cannot be a bare
literal the way `MGL_RESIDUAL_BLOCK_SIZE` (`MGPipeTypes.h:536`) is. Also note the project builds on
Windows/MSVC (this worktree), Android/NDK-libc++ and Linux/libstdc++ — three different answers. See
§5.2 for the three mitigation options.
**R3 — `SharedPtr<glslang::TProgram>` by value in `LinkArtifacts` (ProgramObject.h:1215).** This is
the single member that references glslang by value in the archived types. The incomplete-type plan
(§6.3 option A) is sound in principle but must be compile-verified on all three toolchains; MSVC in
particular has historically been strict about `shared_ptr<Incomplete>` in aggregates with implicit
special members. Fallback is §6.3 option B. Related: the server will deserialize a `LinkArtifacts`
whose `program` is always null — assert that at the deserializer, matching the existing insert-time
assertion documented at `ProgramTranslationCache.h:42–43` ("`link.program` is null by construction …
Asserted at insert").
**R4 — `glslang::TIntermediate::TUniformInitializer` re-typing breaks a comparison.**
`MG_State/GLState/ProgramState/ProgramObject.cpp:180–183` compares `init.basicType` against
`glslang::EbtFloat` / `EbtFloat16` / `EbtDouble` / `EbtInt` / `EbtUint` / `EbtBool`. With
`basicType` widened to `Int` this still compiles (enum→int promotion) but the file keeps a glslang
dependency; that is fine (it is a client TU) as long as the *header* does not. Also
`ProgramLinkTask.cpp:612–618` builds the vector via `std::find_if` on `.name` and copies the whole
struct — a plain type substitution, no field-order sensitivity.
**R5 — `<set>` (MEDIUM, will bite immediately).** `LinkArtifacts` uses `std::set<String>` at :1331
and :1336 and no header on the path includes `<set>` deliberately. Add it explicitly. See §1.4.
**R6 — `kInvalidUniformOffset` ordering (MEDIUM).** Declared at ProgramObject.h:547, *used* as a
default member initializer at :1420. Moving `SpirvArtifacts` without moving the constant is an
immediate compile error, and moving the constant without a re-export breaks 9 call sites across three
modules (§1.5). Handle both halves in one commit.
**R7 — ODR / aliasing (LOW but real).** Injecting `using TypeFacts = ...::TypeFacts;` inside
`ProgramObject` while a namespace-scope `TypeFacts` exists in the same namespace is fine, but
`ProgramLinkTask.cpp:48` writes
`static MobileGL::MG_State::GLState::ProgramObject::TypeFacts MakeTypeFacts(...)` — the
fully-qualified-through-the-class spelling. That resolves through the alias unchanged. Watch for any
place that forward-declares one of these types instead of including the header: grep found none, but
re-check after the move. Also: **do not leave a second definition behind** — the classic P0.5 failure
is copying the struct into the new header and forgetting to delete it from `ProgramObject.h`, which
compiles (different scopes: `GLState::TypeFacts` vs `GLState::ProgramObject::TypeFacts`) and silently
splits the type in two.
**R8 — pImpl / join-gate boundary (LOW, but the invariant to protect).** `LinkArtifacts` and
`SpirvArtifacts` become namespace-scope types, so anyone can now *declare* one without going through
`ProgramObject::Artifacts()`. The I5 invariant (ProgramObject.h:1187–1192, :1622–1631) is protected by
`m_artifacts` being **private**, not by the type being nested — so the invariant survives the move.
Say so in a comment at the top of `LinkArtifacts` in the new header, because the current comment
explains I5 in terms of "the member below is private", and that member no longer lives next to the
struct.
**R9 — `ProgramTranslationCache.cpp:40` takes `sizeof(ProgramObject::ResourceReflection)`.** It feeds
a byte budget, not an allocator ("Approximate on purpose", `ProgramTranslationCache.cpp:46–48`), so a
size change is harmless — but it is a second place that must keep compiling after the alias move.
**R10 — the "7 includers" figure is wrong.** Verified: `ProgramObject.h` has **8** non-self includers
(9 with `ProgramObject.cpp`); `ShaderObject.h` has exactly 7; `SpvcSession.h` has 18. Budget P0.5
against the real numbers (§3), and note that if `ProgramObject.h` drops its `SpvcSession.h` include
(§6.4) the `SpvcSession.h` count falls to 17 while every remaining includer is a
ShaderTranspiler/test/benchmark TU that legitimately wants spirv-cross.
**R11 — fan-out.** `MG_State/GLState/Core.h:16` → `ProgramState.h:12` → `ProgramObject.h` means both
backends and all of `MG_Impl` recompile on any `ProgramObject.h` touch, and *nothing in either
backend directly includes it*. That is the reason the extraction buys real build-time and symbol
separation later — and the reason the `-H` closure assertion should be run against
`MG_Backend/DirectGLES/Managers.cpp` and `MG_Backend/DirectVulkan/DirectVulkan.cpp` (the two big
transitive consumers), not only against `ProgramArtifacts.h` itself.
---
## 8. Quick reference — file inventory
| path | why it matters |
|---|---|
| `MobileGL/MG_State/GLState/ProgramState/ProgramObject.h` | all five types live here today (1803 lines) |
| `MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp` | `ResetLinkArtifacts` :332, `ApplyUniformInitialValues` :143–215, `kInvalidUniformOffset` uses :198/:273 |
| `MobileGL/MG_State/GLState/ProgramState/ShaderObject.h` | 319 lines; 7 includers; the glslang/`ShaderCompileTask` path |
| `MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h` | 18 includers; pulls `<spirv_reflect.h>` :11 |
| `MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h` | `artifacts` :80, `SpirvHandoff` :92–158 |
| `MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp` | sole writer of `LinkArtifacts.program`; `MakeTypeFacts` :48, `MakeResourceReflection` :79 |
| `MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h` | `artifacts` :46 |
| `MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.h` | `ProgramTranslationResult` :49–52 — the existing "whole archive as a value" precedent |
| `MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp` | only consumer that names `LinkArtifacts` outside `ProgramState/` |
| `MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp` | `TypeFactsRef` alias :27–28 |
| `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp`, `Managers.cpp` | Espryt accessor call sites (§4.1) |
| `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp` | SPIRV-Reflect re-derivation :51–305 |
| `MobileGL/MG_Pipe/MGPipeTypes.h` | `MGPProgramDesc` :296–308; the two tripwire idioms :43–45 and :527–539; the P0.5 debt-note convention :24–33 |
| `MobileGL/MG_Pipe/PipeFields.def` | the X-macro field-table prior art :9–18, :83–85, :227 |
| `MobileGL/MG_Pipe/PipeCalls.def:102` | `X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob)` |
| `scripts/gen_pipe.py` | the generator that consumes the `.def` files; `--check` mode is the CI gate |
| `MobileGL/Includes.h` | :79–83 glslang, :56 spirv-cross, :17 `<map>` (no `<set>`), :149 `MG_Util/Types.h` |
| `MobileGL/MG_Util/Types.h` | every alias the new header uses (:21–123) |
| `include/glslang/MachineIndependent/localintermediate.h:627–636` | `TUniformInitializer` — the struct to re-declare |
| `docs/Disaggregated/ARCHITECTURE.md:256–264` | section 7 |
| `docs/Disaggregated/ROADMAP.md:16` | P0.5 row |