Files
MobileGL/docs/Disaggregated/notes/p5e/reports/sb-v1.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

214 lines
14 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.
# sb — `set_shader_buffers`: the binding-point tables cross the wire (P5e wave 2)
Branch `p5e/sb`, head **b004e5dc**, base `f6cfcbd3` (wave 1). 25 files, +1708/-92, two commits:
`1ebe6abe` (wire + applier + route + client emitter + gates + tests) and `b004e5dc` (the four
Espryt consumers).
## Gate
| step | result |
|---|---|
| `build` | rc=0 |
| `unit` | **2216/2216** (2206 at base; +10 new names, each with a pull-build skip twin) |
| `isplit` | **111/111** |
| `gens` | all 7 rc=0; `check_doc_citations` rc=0 with 3 pre-existing warnings in `CONTRACT-P5.md` |
| `one` SSBO/atomic | `DirectGLES.Split.(AtomicCounter\|ProgramPipeline)` 5/5; `DirectGLES.(SsboArrayLength\|SsboArrayDynamicIndex\|StorageBufferRegrow)` 6/6 |
| `one` XFB | `Split\..*Xfb` **11/11** |
| A/B bit 13 cleared | `MOBILEGL_PIPE_PUSH=0x1fff` on `integration-split`: **111/111** — today's picture reproduced |
| `strict` | 3/111 pass, identical to the base (red by design, CONTRACT-P5C §6) |
**Strict marker table, before and after — IDENTICAL, nine rows, same counts:**
`GetFramebufferBindingSlot@Clear` 45 · `GetTextureUnitObject@Clear` 22 ·
`GetBoundVertexArray@DrawArrays` 14 · `GetImageTextureBinding@BindImageTexture` 13 ·
`GetTextureObject@CopyImageSubData` 6 · `GetProgramForDispatch@DispatchCompute` 3 ·
`ValidateProgramName@ShaderStorageBlockBinding` 2 · `GetTextureUnitObject@GenerateMipmap` 2 ·
`GetTextureUnitObject@ReadPixels` 1.
**No `GetBufferBindingPoint@*` marker exists at this head, before or after.** Every split
scenario that binds an indexed buffer aborts on an EARLIER row — pg's `GetProgramForDispatch`
or the allowlisted `ValidateProgramName` — so my family never gets to abort first. That is the
baseline document's own "lower bound" property, and it is why red-once (a) had to be executed
in a different, equivalent form (below). The A/B at `0x1fff` on the **unit** lane has two
failures, `DirectGLESTextureSync.{UnitMemoRefusesToDriveATwinFromAnotherTexture,
AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView}` (SanityTest.cpp:3112) —
NOT this package's: they fail identically with the P5e tracker shutter reverted, and they are
texture-unit-memo cases. The arm the gate asks for (`integration-split`) is green.
## What changed, per file
**Wire / apply** — `MGPipeTypes.h` (`MGPShaderBuffers::WritableMask` → `Uint32[3]`, POD 32→40,
`kMGPipeShaderBufferWritableMaskWords`, `MGPipeShaderBufferMaskHas/Set`); `PipeApply.h`
(`ShaderBufferWritableMask[3][3]`); `PipeApply.cpp` (`MGPipeApplySetShaderBuffers` bodied,
`ClearShaderBufferWindows`, both resets); `PipeRoute.{h,cpp}` + `Client/WireTables.cpp` (op 38,
both arms); `Wire/PipeWireCodec.cpp` (the decode arm applies; Class/Count bounded in the layout
arm); `CapsCodec.cpp` (one static_assert).
**Client** — new `MG_Impl/Pipe/ShaderBufferEmit.h`; `BufferState.h` (`NoteBindPointChanged` /
`GetBindPointGeneration`, push-only); `GLState/Core.{h,cpp}` (the two forwarders, and the DSA
`SetNamedTransformFeedbackBinding` bump); `GL_Buffer.cpp` (the bumps in `BindBufferBase_State` /
`BindBufferRange_State`); `Tracker.h` (bits 15/16/17 rewritten); `PipeFill.cpp` (wired constant,
`P5eFamilyIsLive`, the two adapters, the validate order, the reset list); `MG_Backend/Init.cpp`
(consumer bit 13, ID-106).
**Backend** — `DirectGLES.cpp`: `ResolveBufferBindingSubsystemArm` /
`BindingPointsComeFromRecords` / `SyncBufferBindingPointsByRecord` (all file-local), the two
call sites, `MarkShaderStorageBuffersGpuWritten`'s deletion under a transport,
`SyncAtomicCounterBuffers`' record arm, the UBO loop's point half.
**Gates / tests** — `gen_pipe_dirty_surface.py` (`Touch` prefix, control 6 re-pointed);
`DirtySurface.def` (two rows + the undecided mark); `MG_Test/Pipe/CMakeLists.txt`; new
`ShaderBufferEmitTest.cpp` (8 cases); `TrackerTest.cpp` (2); `PipeCatalogueTest.cpp`,
`PipeWireCodecTest.cpp`, `RemoteClientTest.cpp` (the pins that invert).
**ID-104:** `Uint32 WritableMask[3]`, not `Uint64 + Uint32`. 96 ≥ 84 either way, but ONE word
type means one shift width, so the set and the test are a single expression each rather than a
low half and a high half that have to agree — and the applier's per-class copy goes through the
same two helpers as the payload's. POD 32 → 40, pinned in `PipeCatalogueTest`.
## kimi-audit rows 111–124
| row | site | verdict |
|---|---|---|
| 111 | `SyncBufferBindingPoints` point + `GetBoundObject` | **RETIRED** → `SyncBufferBindingPointsByRecord` |
| 112 | same: ensure probe, `GetRange`, `obj->GetSize` | **RETIRED** → `EnsureBufferResourceForHandle(nullptr, Res)`, the record's extent, `resource->storageSize` |
| 113 | `MarkShaderStorageBuffersGpuWritten` | **RETIRED BY DELETION** under a transport; the client owns the set |
| 114 | `SyncTransformFeedbackBindingPoints` | **LEFT BARRIERED** (XFB, §5.7) |
| 115 | `SyncAtomicCounterBuffers` (count, point, ensure, range, mark) | **RETIRED**; its `MarkBufferGpuWritten` deleted |
| 116 | `SyncBoundBuffer` `GetBufferBindingSlot` | **NOT MINE** — the generic slot, `FieldOwnership.def:78` (P8/P9/P13); untouched |
| 117 | UBO loop point + `GetBoundObject` + `GetRange` | **RETIRED** → `BoundShaderBuffers[Uniform][binding]` |
| 118 | UBO loop `GetBufferResource` / `IsBufferDrawClean` / `EnsureBufferResource` | **RETIRED** → the three by-handle twins |
| 119–124 | `StartPendingTransformFeedback`, `ReadbackCapturedRanges`, `ScatterCapturedRecords` | **LEFT BARRIERED** (XFB, §5.7) |
Not found as written: nothing. `MGPipeFrontendKeyedRegistryScope` at `Managers.cpp:2938`
(`HandleOfBuffer`) is NOT retired by this package — the draw path no longer reaches it for
binding points, but `EnsureBufferResource` / `IsBufferDrawClean` still do for the monolith arm
and for other families.
## Red-once, executed, reverted, quoted verbatim
**(b) the shutter** — reverted bits 15/16/17 to the pre-P5e content aggregate, rebuilt, ran:
```
../MobileGL/MG_Test/Pipe/TrackerTest.cpp:615: Failure
Expected: (m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewConstBuffers)) != (0u), actual: 0 vs 0
NEW_CONST_BUFFERS did not fire when a uniform binding point moved onto another buffer - the emitted window would still name the first one
../MobileGL/MG_Test/Pipe/TrackerTest.cpp:624: Failure
Expected: (m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewConstBuffers)) != (0u), actual: 0 vs 0
NEW_CONST_BUFFERS did not fire for an unbind
../MobileGL/MG_Test/Pipe/TrackerTest.cpp:652: Failure
Expected equality of these values:
m_tracker.LastDirty() & family
Which is: 229376
0u
Which is: 0
a write to a buffer bound to no binding point republished the binding-point sets
```
Both directions in one revert: the old shutter UNDER-fires on a rebind and an unbind and
OVER-fires on an unrelated `glBufferSubData` (229376 = bits 15|16|17). Restored → 2/2 pass.
**(a) the record arm.** The brief's form is not executable at this head (see the strict table
above, and §"rulings"). Executed the equivalent that IS available: keep the server's record arm
and withhold the CLIENT's emission (`kMGPipeWiredBufferBindingSubsystem` back to 0), so the
server's window is empty and nothing else can fill it:
```
0% tests passed, 5 tests failed out of 5
3464..3466 DirectGLES.Split.AtomicCounterScenario.* (Subprocess aborted)
3467..3468 DirectGLES.Split.ProgramPipelineScenario.* (Subprocess aborted)
[08:18:24] [Linux MobileGLIntegra/FATAL]: MGPipe: Fatal{UnimplementedWritebackWait} - a ClientSession is
active and buffer 1 still has an outstanding GPU write after its readback was emitted.
```
Restored → 5/5 pass. Also ran the mirror (`BindingPointsComeFromRecords()` → `false`, the
frontend walk under a live wire): green picture, marker table unchanged — the finding in §2 of
"rulings".
**(c) the sticky forward.** `rsp` is a process-wide residual-pull tally on the apply thread's
stats line, not a per-field one, so the measurable form is the delta on
`DirectGLES.Split.AtomicCounterScenario` (`MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=1`):
```
=== AFTER (record arm live) === rsp values: 2 x rsp=196, 1 x rsp=588 (max 588)
=== BEFORE (frontend walk) === rsp values: 2 x rsp=199, 1 x rsp=597 (max 597)
```
9 and 3 pulls per case removed — `GetBufferBindingPointCount` plus the per-point
`GetBufferBindingPoint` reads of the counter walk. It does not reach 0 because other families
still pull on the same thread.
**(d) no double marking.** The backend marks are deleted; the client's are the only producer.
Withheld `MarkShaderStorageBindings` + `MarkAtomicCounterBindings` on the client:
```
0% tests passed, 3 tests failed out of 3
../MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp:186: Failure
Expected equality of these values: Which is: 5 Which is: 13
../MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp:189: Failure
Expected equality of these values: Which is: 100 Which is: 116
```
Restored → 3/3 pass, and `SplitBufferTest`'s existing
`ProducerMarkCount(ShaderStorageBinding) == 1` / `AtomicCounterBinding == 1` pins stay green
(24/24). A revert of the backend DELETION cannot go red by construction — it is a duplicate —
so this is the honest form of (d).
**(e) the catalogue inverts.** `PipeCatalogue.{UninstalledTablesAreAllNull,
ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull}` now assert `SetShaderBuffers != nullptr`; installed rows
34 → 35, the escape total 38 → 39, `PipeRouting...EveryRoutedRowMoved` 34 → 35.
## G1 / G2 / G14
Every new client symbol is under `MOBILEGL_PIPE_PUSH` (`BufferState`'s two members and the two
`GLContext` forwarders included, so the pull build's objects do not resize) and every new server
arm under `MOBILEGL_BUILD_DISAGGREGATED`, with the frontend arm kept token for token for
`Transport=monolith` (ruling 1). The only shared-code deltas are `MGPShaderBuffers`' size (a
push-only payload) and two `static_assert`s. 10 new ctest names, all with pull-build skip twins.
## Seams I assumed
- **pg** owns `currentProgram->GetUniformBlockBinding(i)` (`DirectGLES.cpp`, the line above my
arm). I index `BoundShaderBuffers[Uniform][binding]` with its result and did not touch it; if
pg moves the binding source to `MGPipeShaderCsoRecord::BlockBindings`, `binding` keeps its
meaning and my arm is unaffected.
- **fb** owns the third GPU-write mark, `MarkWritableImageBufferTexturesGpuWritten`
(`DirectGLES.cpp`, kimi row S3). I deleted my two under a transport and left it; the brief
calls the pair "one edit split across two files".
- **ra** flips `kMGPipeP5eClientWaitRuleLanded`. `SetShaderBuffers` is `kWaitNone` in c0e's
static column, so it goes fire-and-forget on that day with no change here.
- **tx2 / fb** own the rows that abort ahead of mine in the strict lane.
## Rulings I need
1. **`GetBufferBindingPointCount`'s forward → FATAL under split (§5.6, §8.8) CONFLICTS WITH §6.**
`MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:1200` and `:2030` are live Magma
readers, `FieldOwnership`'s class is unconditional, and §6 keeps Magma's P5C semantics — so
the flip would abort a split Magma server. **I left both rows `BARRIER_PULLED`** and removed
Espryt's only caller instead (the measured `rsp` drop above). A backend-keyed class would be
a new mechanism and an integrator ruling.
2. **Red-once (a)'s scenarios do not exist in the split lane.** `SsboArrayDynamicIndexScenario`,
`StorageBufferRegrowScenario` and `SsboArrayLengthScenario` carry only `integration-gpu`;
there is no `DirectGLES.Split.` twin. In the split scenarios that DO bind indexed buffers the
strict lane aborts on pg's row first, so `Fatal{UnmigratedPipeInput,
"GetBufferBindingPoint@DrawVbo"}` is unreachable until tx2/fb/pg land. Re-run this red on the
integrated tree (BRIEF §3.5) — it should be reachable there.
3. **The sink named in the brief does not exist.** `MG_Remote/Server/PipeApplier.cpp`'s
`ServerVerbSink` has methods for the five class-B VERBS only; a `kCtxState` row's sink is the
decode arm in `MG_Remote/Wire/PipeWireCodec.cpp`. I extended scope to two arms of that file
(the decode case and the layout bound). pg will touch the neighbouring `SetProgramBindings`
case — separate `case` labels, so the merge is textual only.
4. **`PipeFill.cpp` needed a conjunct outside the named ranges.** `P4aFamilyHasItsConsumer` asks
R-8 for the RESOURCE bit and answers `true` for any non-P4a family, so ID-106's consumer bit
13 would have had no reader. I added `P5eFamilyIsLive` (bit 13's own R-8 question + bit 13
requires bit 7) beside it and one conjunct in `FamilyIsLive` and in `wants()`.
5. **`CONTRACT-P5E.md` §5.6 still says the `WritableMask` question is "an integrator ruling sb
must ask for rather than pick".** ID-104 answered it; the landed contract text wants that
paragraph replaced at landing. I did not edit c0e's file.
6. `Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}` is spelled in the file's existing shape
(`Fatal{ProtocolCorruption} SetShaderBuffers.Class - …`, as `ApplyUnitWindow` spells its
own), not with the contract's comma-and-quotes. Say if the contract's literal form is wanted.
## What I did not do
- The program-derived `set_shader_buffers` window (BRIEF §5, trailing): the window is the
touched high-water mark, per ruling 10. A program binding block 0 at GL point 83 still pays an
84-entry record on its first emission after a bind.
- `set_stream_output_targets` stays unemitted and XFB stays lockstep (§5.7). Dirty bit 17 is
computed, shutters correctly on the new generation, and names the subsystem.
- `FieldOwnership.def:80-81` stays `BARRIER_PULLED` with phase "P5e (Espryt unbarriered), P7
(Magma)": Magma still reads `GetBufferBindingPoint`, so the four consumers are not all gone.
- No `PipeStats::CallClass` row for binding-point emissions — that would grow the counter arrays
and the stats line for every build. The emitter keeps its own per-class tally
(`EmissionCount(cls)`), which is what the unit suite reads.