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.
14 KiB
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_asserts. 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 indexBoundShaderBuffers[Uniform][binding]with its result and did not touch it; if pg moves the binding source toMGPipeShaderCsoRecord::BlockBindings,bindingkeeps 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.SetShaderBuffersiskWaitNonein 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
GetBufferBindingPointCount's forward → FATAL under split (§5.6, §8.8) CONFLICTS WITH §6.MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:1200and:2030are 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 rowsBARRIER_PULLEDand removed Espryt's only caller instead (the measuredrspdrop above). A backend-keyed class would be a new mechanism and an integrator ruling.- Red-once (a)'s scenarios do not exist in the split lane.
SsboArrayDynamicIndexScenario,StorageBufferRegrowScenarioandSsboArrayLengthScenariocarry onlyintegration-gpu; there is noDirectGLES.Split.twin. In the split scenarios that DO bind indexed buffers the strict lane aborts on pg's row first, soFatal{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. - The sink named in the brief does not exist.
MG_Remote/Server/PipeApplier.cpp'sServerVerbSinkhas methods for the five class-B VERBS only; akCtxStaterow's sink is the decode arm inMG_Remote/Wire/PipeWireCodec.cpp. I extended scope to two arms of that file (the decode case and the layout bound). pg will touch the neighbouringSetProgramBindingscase — separatecaselabels, so the merge is textual only. PipeFill.cppneeded a conjunct outside the named ranges.P4aFamilyHasItsConsumerasks R-8 for the RESOURCE bit and answerstruefor any non-P4a family, so ID-106's consumer bit 13 would have had no reader. I addedP5eFamilyIsLive(bit 13's own R-8 question + bit 13 requires bit 7) beside it and one conjunct inFamilyIsLiveand inwants().CONTRACT-P5E.md§5.6 still says theWritableMaskquestion 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.Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}is spelled in the file's existing shape (Fatal{ProtocolCorruption} SetShaderBuffers.Class - …, asApplyUnitWindowspells 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_bufferswindow (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_targetsstays 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-81staysBARRIER_PULLEDwith phase "P5e (Espryt unbarriered), P7 (Magma)": Magma still readsGetBufferBindingPoint, so the four consumers are not all gone.- No
PipeStats::CallClassrow 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.