# pg — programs (P5e wave 2), v1 Branch `p5e/pg`, head **4d1499ef**, base **f6cfcbd3**. 3 commits, 18 files, +2071/-316. Gate: build 0 · unit **2210/2210** · integration-split **111/111** · gens all rc=0 · `one` on the six named scenarios **212/212**. Strict lane run before AND after (§5). ## 1 What changed, per file | file | region | what | |---|---|---| | `ProgramArtifactsCodec.{h,cpp}` | header note, `:242` note, +55 tail | names the skipped member + reads the libc++ caveat (§2); `ProgramArchive`, `kProgramArchiveMaxStages`, `Encode/DecodeProgramArchive` — the FRAME `[stageCount][stages][codec stream]`, bounded before it multiplies, refusing a stage count that disagrees with the module count | | `MG_Pipe/PipeApply.h` | `:41-52`, `:384-420`, `:1327-1345` | `ProgramArchive` fwd-decl; `MGPipeShaderCsoRecord::Archive`; `MGPipeApplyCreateShaderState`'s 4th argument | | `MG_Pipe/PipeApply.cpp` | `:2854-2918`, `:3000-3060` | `MGPipeApplySetProgramBindings`'s body; the create ADOPTS the archive and clears the three tails + `Signature`, advancing `BindingsSerial` | | `MG_Pipe/PipeRoute.{h,cpp}` | escape table + 2 sites | the FIFTH escape (§7.3); `CreateShaderState` grows the stage list; `MGPipeRouteSetProgramBindings` | | `MG_Remote/Client/WireTables.cpp` | `:488-600` | the create frames the archive and counts its bytes into PipeStats; new `Wire_Escape_SetProgramBindings` (three tails, per-name SEG_STAGE staging) | | `MG_Remote/Wire/PipeWireCodec.cpp` | the two arms | decodes onto the heap into the record's own archive; op 80 resolves and NUL-checks every name span and calls the applier | | `MG_Impl/Pipe/ProgramEmit.h` | `:90-110`, `:106-180`, `:215-330` | `MGPipeStorageOverrideSignatureEntry`; `EmitProgramBindings` + latch; `Desc.LinkStatus`; the stage list; the bindings latch cleared on re-issue | | `Managers.h` | `:2341-2480` new + 6 signatures | `ProgramArchiveSource`, `ProgramBuildSource`, `ProgramHandleArm()`, `m_syncedBindingsSerial`, `ProgramBlockBindingFromRecord` / `ProgramSamplerUnitFromRecord` | | `Managers.cpp` | `:745`, `:10913-11060`, `:11123-11370`, `:11907-13400` | the source's two constructors and its accessors; every helper of the build re-typed; `SyncToBackendFromSource` + the two public heads; the seam stub deleted | | `DirectGLES.cpp` | `:4097`, `:4230-4620`, `:4800-4830`, `:5460-5800`, `:5900-6030`, `:6400-6430` | the handle stash; `ResolveGlobalConstantsRecordForHandle` + the MapUBO refusal; `ResolveFragColorBroadcastCount`'s policy; `SyncCurrentProgramByHandle`; both Prepare lines; `BindCurrentProgramWithResources`; `GetCurrentBackendProgram`; `CurrentProgramMayNeedPerSubDrawBuiltins` | | tests | `ProgramEmitTest` +4 cases; 5 files mechanical | §7.5 | **The one-body decision (G1).** `SyncToBackend`'s ~1070-line body is written ONCE against a reference `src` whose TYPE is `PrgramImpl::ProgramBuildSource` — `ProgramArchiveSource` in a push build, `ProgramObject` in a pull build. The view's accessors carry ProgramObject's names one for one, so the pull build compiles the same text through the same accessors. The alternative was an `#if`-duplicated 1100-line body. **Exact pull deltas: §6.** ## 2 Codec completeness (CONTRACT-P5E §5.5) The skipped member is **`LinkArtifacts::program`**, the live `SharedPtr` that `VisitFields` omits (57 of 58). **Proven unread by either twin, by closed grep:** every read in the tree is inside `ProgramLinkTask.cpp` (`DoReflection`) plus the three `reset()` sites; `MG_Backend/` contains none, and `ProgramTranslationCache` asserts it null at insert. It could not be carried in any case — it points into a glslang arena no archived instance owns. `ProgramArtifactsCodec.cpp:242`'s "libc++ caveat" is about the **struct-size ECHO**, not a member: under the NDK's unpinned branch the word is 0, which is exact for an in-process split (one build writes and reads) and becomes a real check at P6's process boundary. Both verdicts are written where they were asked for. ## 3 Kimi audit rows 91-110 **Retired** (record/handle answers them now): 93 (`Desc.LinkStatus`/`SpirvStatus`), 94 (`ResolveProgramTwin`), 96 (`record.Serial`/`BindingsSerial`), 97 (`record.Signature`), 98 (`SyncToBackendByHandle`), 99 (`ResolveGlobalConstantsRecordForHandle`), 101, 102 (`Desc.GlobalUboSize`/`GlobalConstantsVersion`), 103 (**`MapUBO` is Fatal now**, gap G-C), 104 (`ProgramBlockBindingFromRecord`), 105 (`BindingsSerial` + `ProgramSamplerUnitFromRecord`), 107, 108, 110 (handle arm; kept as the monolith stamp). **Left pulled, and why:** 91/92 (`GetProgramForDraw`/`ForDispatch` — my sync no longer reads them, but the accessor CALL stays for vi's and tx2's consumers, §4.3); 100 (`BindCurrentTextures`' memo — tx2's line range, §4.1); 106 (vi's); 95 (`MOBILEGL_PIPE_LEGACY_MEMOS`, unreachable under a transport); **109 left barriered BY CONTRACT §5.5** — `GetBackendProgramId` is a GL-thread entry point (P9) and `ShaderStorageBlockBinding` is reached from the barriered `set_storage_block_binding` (ID-85, pg's trailing item). **Not found: none** — all twenty rows exist at this head. **The five scope sites → three retired, two kept.** `ResolveGlobalConstantsRecord`, `SyncCurrentProgram` and `BindCurrentProgramWithResources` are unreachable under a transport (the scopes stay as the monolith glue's, ruling 1); the other two are row 109's contract exceptions. ## 4 Red-once evidence (executed, quoted verbatim, reverted) **(1) Empty the applier's block-binding tail.** The two named scenarios are NOT in `integration-split` (they are `DirectGLES.*`, the monolith lane), and with the tail emptied the split lane stays **111/111 green** — no split scenario binds a named uniform block to a non-zero point. The statement moved to the unit case the brief also names: ``` ../MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp:894: Failure Which is: 0 Which is: 3 [ FAILED ] ProgramEmit.TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem ``` **(2) Blob refs at Size 0, companion pointers removed** (decoder passes `nullptr,nullptr,nullptr`); `Split.TriangleScenario.AVboBackedTriangleReachesReadPixels` aborts: ``` [Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{ProtocolCorruption} create_shader_state {slot=1, gen=0}: the record declares no blobs and carries no artefacts (stages=0x11, globalUboSize=0, bound 16777216) ``` **(3) The frontend-keyed resolution back, id's hook forcing "unbarriered".** `PrepareForDraw` routed to `SyncCurrentProgram` + `MGPipeApplierSetCurrentRecordBarriered(false)` at its top — ID-102 kept: the probed action IS the registry lookup, not an object built on the apply thread. ``` [Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{RoleViolation, "MGPipeSlots"} - the apply thread called MGPipeSlots().HandleOf. With an active transport the client slot allocator is client-only memory (CONTRACT-P5C §3.1, rule E; CONTRACT-P5E §4.4): a handle arrives already minted in a record, and a server that resolves or mints one off a frontend object's lifetime id is reading memory that will not exist on its side of a real split. A named exemption scope admits it only while the record being applied is BARRIERED (barriered=0) and, for Magma's P7 debt, only on a DirectVulkan server ``` **(4) `strict` names `GetProgramForDraw@draw_vbo` — NOT DEMONSTRABLE AT THIS HEAD**, and the reason is structural rather than a miss: see §5. The marker is in NEITHER table. **(5) The global-constants record declines under run-ahead** (what forcing bit 12 off does to the record arm); 8 split entries abort: ``` [Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{UnmigratedVerb, "set_global_constants"} - the ShaderCso record {1, 0} does not answer the default uniform block for a draw of a program that declares 16 bytes of it, and MapUBO() is the client's live scratch array ``` ### ID-87's measurement — numbers, not an argument `EncodeProgramArchive` instrumented at the one place the bytes exist; whole split lane, **80 links**, then reverted: ``` us: min=3.2 p50=4.5 p90=5.4 max=6.4 mean=4.6 bytes: mean=2516, max observed 6070 ``` PipeStats over `TriangleScenario.*` independently: `cso-blob-bytes = 4538` for 2 links. **Links per steady frame = 0** — the re-issue latch is `GetLinkVersion()`, which only a real `glLinkProgram` moves, so the cost lands on loading frames. Slope ≈ **1 µs per KB**, so ruling 8's 0.2 ms/frame trigger needs ~200 KB of archive linked in ONE frame (~33 of the largest measured). **The half ID-87 actually asked for — MC + a shader pack on the Redmi — IS NOT DONE:** no device here and adb is forbidden by the package instructions. These are lavapipe with the lane's small programs, and I will not extrapolate a ruling trigger from a 6 KB sample. **Blocker for declaring ruling 8 settled**: run the device arm at the phase gate, or land lazy-encode as a precaution. ## 5 The strict lane, before and after Run on this tree at **f6cfcbd3** and at **4d1499ef**. Both: **108/111 failed, 3 passed**; 88 of 111 entries keep a private log (the ID-53 baseline saw 22 — the per-entry log path now covers the whole lane, so this is a wider slice than `STRICT-BASELINE-2fde7034.md`'s). | marker | before | after | owner | |---|---|---|---| | `GetFramebufferBindingSlot@Clear` | 39 | 39 | fb | | `GetBoundVertexArray@DrawArrays` | 14 | 14 | vi | | `GetImageTextureBinding@BindImageTexture` | 13 | 13 | fb | | `GetTextureUnitObject@Clear` | 9 | 9 | tx2 | | `GetTextureObject@CopyImageSubData` | 6 | 6 | tx2/fb (trailing) | | `GetProgramForDispatch@DispatchCompute` | 3 | 3 | **pg**, blocked on tx2 (§4.3/§7) | | `ValidateProgramName@ShaderStorageBlockBinding` | 2 | 2 | **pg**, allowlisted trailing (ID-85) | **IDENTICAL, and that is the honest result.** Each entry aborts on its FIRST pulled row, and in every entry of this lane a framebuffer, VAO, texture or image row precedes any program read — so no program field of mine was ever first, none could disappear, and none surfaced behind one. `GetProgramForDraw@draw_vbo` appears in neither. This lane will only measure this family once fb, vi and tx2 have landed; what pg can show is the three executed Fatals above. ## 6 G1 / monolith impact — the deltas, named Every new symbol is under `MOBILEGL_PIPE_PUSH` / `MOBILEGL_BUILD_DISAGGREGATED`. **I could not run the comparator** (`symbol_report.py` needs pull builds of base and head; the gate script has no G1 step). The deltas the pull build WILL see, from the diff: 1. `SyncToBackend(const SharedPtr&)` keeps name and signature; its body gains `const ProgramBuildSource& src = *stateProgramObject;` and every read becomes `src.X()`. Same loads, same inlining **expected — an expectation, not a measurement**. 2. `CacheResourceLocations`: `(const SharedPtr&)` → `(const ProgramObject&)` — mangled-name change + a dropped `SharedPtr` deref. 3. `RebindImageUniformsToFrontendUnits`: likewise, and it loses one null test the caller already made. 4. The other six helpers keep their signatures exactly (`ProgramBuildSource` IS `ProgramObject` there). Expected pull report: **2 renamed + 2 resized, 0 added, 0 removed**, all in one function family. **Ruling wanted:** accept those four, or ask for the `#if`-duplicated body. I recommend accepting — the duplicate is 1100 lines two arms would have to be kept in step by hand for ever. Push + `Transport=monolith` keeps its frontend arms through `ProgramHandleArm() = Transport != Monolith && ProgramSubsystemEnabled()`. ## 7 Seams, assumptions and rulings needed 1. **tx2 — `BindCurrentTextures`' memo key (ruling 6 / ID-86).** Its range, so I left a comment on `ResolvedTextureBindingMemo` instead of editing. **What I expect:** the four program fields replaced on the handle arm by `{DrawProgram.Slot, .Gen, record Serial, BindingsSerial}`, AND-ed with tx2's `SamplerViewsSerial`. Both serials are needed — a relink with unchanged texture resolution still hands the twin new unit assignments. 2. **tx2 — `ResolveAndBindUnitTextures`/`sampledTargetForUnit` (`:4834-4948`).** BRIEF gives the range to tx2, §5.5 gives the function to pg; I left it to tx2. `PrgramImpl::ProgramSamplerUnitFromRecord(record, location)` and `ProgramArchiveSource`'s uniform-type accessors are public for it. 3. **vi + tx2 — `GetProgramForDraw`/`ForDispatch`.** `:4781` and `:6126` are my lines but their consumers are vi's `SyncCurrentVertexAttributeValues` and tx2's `BindCurrentTextures`; the call can only become conditional once both stop taking a `SharedPtr`. 4. **sb — the UBO loop.** I own `:5463` (the block binding, from the record's first tail) and left `:5464-5497`; `binding` is still a `Uint` in the same index space. 5. **Files touched outside my listed scope, all mergeable:** `PipeWireCodec.cpp` (op 80's own `case` — `PipeApplier.cpp` has no `OnSetProgramBindings`; every non-verb row's dispatch lives in `ApplyChecked`, so the task file's pointer is an estimate); `PipeCatalogueTest.cpp` and `RemoteClientTest.cpp` (escapes 4→5, total 38→39 — op 80's `gMGPipeContext` slot STAYS null, so those pins are unchanged); `PipeWireCodecTest.cpp` (the create stages the FRAMED archive); `ResourceEmitTest.cpp`/`CompositeResolverTest.cpp` (the create's 4th argument, `nullptr`). 6. **RULING: the emission ORDER is the opposite of the draft's note.** `MGPipeTypes.h`'s `MGPProgramBindings` comment says "Emitted BEFORE create_shader_state". That is wrong against the applier the same contract specifies: a re-issued create CLEARS all three tails, so bindings published ahead of it are wiped. I emit them AFTER the create at the same validate point and said so in both files. **That c0e comment still says the wrong thing and needs a one-line fix.** 7. **RULING: one signature authority per arm** — the handle arm stamps and compares the CLIENT's `record.Signature` verbatim, so the two commutative hashes never have to stay bit-identical. Confirm, or the equality needs a test that can reach both (nothing in `MG_Test` can today: MG_Backend includes nothing from MG_Impl). 8. **`MGPipeApplyCreateShaderState` grew a fourth argument, undefaulted**, on PipeRoute.h's stated reasoning; five call sites moved, three in other packages' test files. 9. **ID-87 is not settled** (§4). ## 8 What I did not do * `set_storage_block_binding` by `ShaderCso` — the TRAILING item, explicitly not this package (ID-85); `ShaderStorageBlockBinding`'s monolith body is kept verbatim. * `BindCurrentTextures`' memo and `ResolveAndBindUnitTextures` — tx2's ranges (§7.1-2). * The two `GetProgramFor*` accessor calls — blocked on vi and tx2 (§7.3). * The composite uniform-mirror fire rate (BRIEF §5) — still unmeasured. The mechanism is the latch on `(backendStateVersion, blockBindingVersion)` and both setters' equality bail-outs, so a mirror re-setting unchanged values emits nothing; that is the mechanism, not a measurement. * The G1 comparator run (§6) and ID-87's device arm (§4).