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.
28 KiB
BRIEF-P5E — the package plan for retiring the lockstep on the Espryt draw path
Contract: CONTRACT-P5E-draft.md (same directory; becomes MG_Remote/CONTRACT-P5E.md at c0e).
Base feat/disaggregated @ 2fde7034. Paths under MobileGL/. Eight packages; seven of them can be
implemented in parallel worktrees once c0e and id have landed. Every line-range below is at the
base head and names the ONLY region the package may edit in a shared file; a package that needs a
line outside its range asks the integrator, who either widens the range or lands the edit in c0e.
0 The shape of the landing, in one paragraph
Nothing on Minecraft's steady draw path can go fire-and-forget until EVERY per-draw family reads
records instead of the frontend: PrepareForDraw touches the VAO (vi), the program (pg), the unit
textures and samplers (tx2), the bound FBO and its attachment lists (fb) and the UBO points (sb) on
every draw, hit or miss. So the smallest first landing that lets the client stop waiting at draws
is c0e + id + {vi, sb, pg, tx2, fb} + ra, with the caps bit flipped in ONE integration commit after
all of them. What makes it small is what is left barriered and trailing (§5): XFB, client vertex
arrays, CopyTex / GetTexImage / readbacks, set_storage_block_binding, the texture half of
resource_subdata, set_texture_params' reply — none of which is on the steady path. The
contract's barriered predicate (§2.1) is what lets those keep P5C's semantics while draws run
ahead, and the unconditional detector (§3.3) is what makes a missed site a named abort instead of
a wrong picture.
1 Dependency order
c0e (the contract, the wire rows, the knobs; inert) -- lands FIRST, no behaviour change
└─ id (registry rekey, by-handle resolvers, the guard) -- lands SECOND, lockstep unchanged
├─ vi (VAO / VBO / IBO) ┐
├─ sb (set_shader_buffers, binding points) │ parallel worktrees, any order,
├─ pg (program twin, archive, bindings) │ each rebased on id
├─ tx2 (textures, samplers, unit windows) │
└─ fb (FBO, attachments, images, blit) ┘
└─ ra (the wait rule, credit, gPipeInputs, flow control) -- parallel from day one on c0e;
its LAST commit is the integration
commit: kMGPipeP5eRunAheadReady=true
Cross-package couplings that must be known before branching: pg's BlockBindings tail is what sb's
UBO loop indexes with (DirectGLES.cpp:5463 vs :5464); tx2's SyncMipmapsToBackendByHandle /
SyncTextureToBackendByHandle are what fb's attachment sync and image sync call; fb's reverse
index is what tx2's ScopedDetachedTextureFramebufferAttachments needs; sb's deletion of the
backend GPU-write marks and fb's deletion of MarkWritableImageBufferTexturesGpuWritten are one
edit split across two files. Each such seam is a declared signature in c0e (a header declaration
with a body that asserts "not landed") so both sides compile from day one.
2 Packages
c0e — the contract and the wire (integrator; lands first)
Scope. MG_Remote/CONTRACT-P5E.md; MG_Pipe/PipeCalls.def (the WaitClass column on every row,
SetProgramBindings opcode 80 appended); scripts/gen_pipe_wire.py (or whichever generator emits
PipeWire.inc) + regenerated MG_Pipe/generated/*.inc (MGPipeWaitClassFor, the new op's thunk /
table slot / codec layout, route row null); MG_Pipe/MGPipeTypes.h (kCapRunAheadApply,
MGPProgramBindings + tail PODs, MGPProgramDesc::LinkStatus, kDrawClientArrays,
kMGPipeMaxBufferBindingPoints); MG_Pipe/MGPipeValueTypes.h (MGPipeImageAccess enum, encode/
decode); MG_Pipe/MGPipe.h (bit 13, default mask 0x3fff); MG_Impl/Pipe/SetHashSuppressor.h
(four slots); MG_Impl/Pipe/Tracker.h:97-99, 204-209 (bits 15-17 → subsystem 13); Config.h
IpcTable::RunAhead/PresentCredit; ConfigLoader.cpp:356-403; MG_Pipe/PipeApply.h declarations
of every cross-package seam (MGPipeApplierCurrentRecordIsBarriered(), MGPipeBarriered(...), the
sb/pg applier fields, the by-handle sync signatures) with asserting bodies; MG_Pipe/FieldOwnership.def
re-annotations; MG_Pipe/Coverage.def rows; MG_Test/Pipe/PipeCatalogueTest.cpp (sizes, opcode
80, the null route pins); MG_Test/Pipe/TrackerTest.cpp:385-387 (bit 13); docs/Disaggregated/ ROADMAP.md row; MG_Backend/Init.cpp (kMGPipeP5eRunAheadReady = false, the DirectGLES arm that
will publish bit 10). Red-once. The ABI fingerprint (SessionRings.h:498-524) changes with the
new opcode and PipeCatalogueTest pins the new sizes; MagmaPipeIdentityTest pins no bit 10 on
DirectVulkan. Quick gate. Unit green; integration-split green (nothing behaves differently);
G1 0/0/0/0.
id — identity (S7's spine; lands second)
Scope. MG_Backend/DirectGLES/SlotTables.h (composite band m_band; ForEachLive re-typed as
fn(MGPipeHandle, const BackendPtr&); the frontend-keyed members kept as monolith glue, each
asserting !MGPipeApplierIsUnbarrieredApply() in split builds); Managers.h:309-640 (registry
forwarding, ResolveVaoTwin(handle) / ResolveProgramTwin(handle) / ResolveTextureTwin(handle)
declarations in the ResolveSamplerCsoTwin shape, AdoptTwinByHandle as the body);
Managers.cpp:188-372 (teardown, OnFrontendStateObjectDestroyed — delete the NoSession hop
:224-246 and the scope at :256; ReleaseTwinsForWireObjectDeath); MG_Impl/Pipe/SlotAllocator.{h,cpp}
(the guard: exemption iff scope AND barriered record; MGPipeFrontendKeyedRegistryScope kept as a
class until the family packages delete their sites; MGPipeReverseAnnouncementScope →
MagmaP7AllocatorDebtScope, backend-kind-keyed); MG_Backend/DirectVulkan/VulkanRenderer.cpp:4554, 4634, 8986 + MG_Impl/Pipe/ResourceTracker.h:723 (the rename only); MG_Remote/Server/PipeApplier.cpp
ApplyOne sets CurrentRecordBarriered from MGPipeBarriered (the one line ra also touches —
id owns it, ra rebases); MG_Test/SanityTest.cpp:3499-3960, 4061, 4241, 4378, 4898 (cases moved to
the legacy arm or rewritten by handle; new ACompositeHandleDoesNotGrowTheOrdinaryTable);
MG_Test/Wire/RemoteClientTest.cpp:1866-1913 (...IsAllowed deleted; new
...FromTheApplyThreadIsFatalEvenAtTheOldDebtSites posting HandleOf through RunOnApplyThread
with the barriered flag forced false by a test hook). Red-once. With the hook forcing
"unbarriered", HandleOf from the apply thread aborts Fatal{RoleViolation, "MGPipeSlots"}; put
the exemption back → the case is green → red by expectation. Quick gate. Unit + integration-split
green (lockstep behaviour unchanged: every record is barriered until ra). New names (G2/G14):
ResolveVaoTwin/ResolveProgramTwin/ResolveTextureTwin(MGPipeHandle), MagmaP7AllocatorDebtScope,
MGPipeApplierCurrentRecordIsBarriered — all MOBILEGL_BUILD_DISAGGREGATED.
vi — VAO / vertex input / index (S1)
Scope. DirectGLES.cpp:699-925 (SyncVaoAttributeBuffersByHandle, the IBO arm), :1602-1719
(ResolveVaoTwin handle arm, SyncCurrentVAO, SyncCurrentVertexAttributeValues), :4770-4776
(the VAO lines of PrepareForDraw ONLY), :6447-6480 (restart helpers), :6718-6782
(DrawArrays/MultiDrawArrays arms: monolith-only); Managers.h:1204-1300 (ResolvedDrawBuffers:
frontend leaves the split arm, iboSerial added), :1286-1290; Managers.cpp:4881-4892, :5160-5350
(SyncToBackendFromApplier — signature only), :5364-5468 (SyncClientSideAttributesForDrawArrays:
monolith-only); MG_Remote/Client/EmitTables.cpp:484-525, 640-690 (kDrawClientArrays + the
run-ahead refusal beside CLIENT_INDICES); MG_Test/Pipe/VertexInputEmitTest.cpp + a
SanityTest.cpp sibling beside :4083. Trailing inside vi: ResolvedDrawBuffers deletion after a
measurement. Red-once. (1) Revert ResolveVaoTwin to Find(vao.get()) → Fatal{RoleViolation, "MGPipeSlots"} on the first integration-split draw once ra flips the bit; before that, the id
test hook. (2) Publish set_vertex_buffers for A,B, repoint the frontend VAO at C without
re-emitting, sync → A,B ensured; the revert ensures C. (3) Same on set_index_buffer. (4)
glDrawArrays with a client array under run-ahead → the named refusal; revert → red under
integration-split-strict (DoublePrecisionScenario if it uses client arrays, else a new scenario).
Quick gate. Unit + integration-split green; VertexAttribBindingScenario,
VertexArrayEnableDisableScenario, PrimitiveRestartScenario, ResidentIndexScenario,
MultiDrawScenario, DrawParametersScenario, HandleRecycleScenario, ObjectSubsystemControlScenario
(the 0x0ff/0x17f A/B keeps its verdicts). New names: ResolveVaoTwinByHandle (or the id
resolver), SyncVaoAttributeBuffersByHandle's new signature.
sb — buffer binding points (S5)
Scope. MG_State/GLState/BufferState/BufferState.h:69-75 (bind-point generations, push-only);
MG_Impl/GLImpl/Buffer/GL_Buffer.cpp:1501-1520, 1624-1650 + SetNamedTransformFeedbackBinding (the
bumps); MG_Impl/Pipe/Tracker.h:520-560, 630-640 (bits 15/16/17 shutters); new
MG_Impl/Pipe/ShaderBufferEmit.h; MG_Impl/Pipe/PipeFill.cpp (SubsystemForEmitter :1989-2028
row + pairing asserts :2104-2126, the validate order :3225-3233, the reset list :3129-3143,
the kMGPipeMaxBufferBindingPoints/84 pin); MG_Pipe/PipeApply.{h,cpp} (MGPipeApplySetShaderBuffers,
the applier fields declared by c0e); MG_Pipe/PipeRoute.{h,cpp} + MG_Remote/Client/WireTables.cpp
(route rows for op 38); MG_Remote/Server/PipeApplier.cpp (the one sink method OnSetShaderBuffers);
DirectGLES.cpp:477-673 (SyncBufferBindingPointsByRecord, MarkShaderStorageBuffersGpuWritten
deleted under transport, SyncAtomicCounterBuffers), :979-990 (call lines), :5464-5497 (the
point half of the UBO loop; :5463 is pg's); scripts/gen_pipe_dirty_surface.py:87-90 + DirtySurface.def
rows; MG_Test/Pipe/TrackerTest.cpp, new ShaderBufferEmitTest.cpp, PipeCatalogueTest.cpp:170-171, 242-243 (invert); MG_IntegrationTest A/B with bit 13 cleared. Red-once. (a) Revert the handle
walk under a live wire → Fatal{UnmigratedPipeInput, "GetBufferBindingPoint@DrawVbo"} on
SsboArrayDynamicIndexScenario / StorageBufferRegrowScenario / AtomicCounterScenario under
strict. (b) TrackerTest: glBindBufferBase(UNIFORM,1,A); Update(); glBindBufferBase(UNIFORM,1,B); Update() sets bit 15 on the second update — red against Tracker.h:636 today. (c)
GetBufferBindingPointCount's rsp non-zero before, 0 after, on AtomicCounterScenario. (d)
Delete the backend marks and ProducerMarkCount(ShaderStorageBinding) stays non-zero while
SsboArrayLengthScenario's readback sees the writes. Quick gate. Unit + integration-split +
strict on the four SSBO/atomic scenarios and the three XFB ones; the bit-13-cleared A/B reproduces
today's picture. New names: MGPipeApplySetShaderBuffers, MGPipeShaderBufferEmitterInstance,
SyncBufferBindingPointsByRecord (file-local), BufferState::NoteBindPointChanged.
pg — programs (S4)
Scope. MG_Impl/Pipe/ProgramEmit.h (archive encode per link via ProgramArtifactsCodec into
SEG_STAGE; EmitProgramBindings; LinkStatus; the failed-relink rule); MG_State/GLState/ProgramState/ ProgramArtifactsCodec.{h,cpp} (completeness: name :242's skipped member); MG_Pipe/PipeApply.{h,cpp}
(MGPipeApplyCreateShaderState adopts the blobs into record-owned artefacts — the companion
pointers go under a transport; MGPipeApplySetProgramBindings); MG_Pipe/PipeRoute.* +
WireTables.cpp (op 80 route); PipeApplier.cpp (OnSetProgramBindings; trailing:
set_storage_block_binding by ShaderCso); DirectGLES.cpp:4097-4110 (the stash), :4230-4318
(ResolveGlobalConstantsRecord(handle), MapUBO refusal), :4320-4510 (SyncCurrentProgram(handle)
incl. the broadcast decline arm :4383-4404 → Fatal under transport), :4781 (PrepareForDraw's
program line), :5317-5560 except :5464-5497 (sb) and :5561-5632 (tx2), :5670-5740
(GetCurrentBackendProgram), :6120-6130 (PrepareForCompute's program line), :10018-10042
(ShaderStorageBlockBinding: monolith body kept; trailing handle arm); Managers.h:2338-2541;
Managers.cpp:10757-13080 (SyncToBackend(h, record), CacheResourceLocations,
ReseedShaderStorageBlockBindings, the signature); MG_Test/Pipe/ProgramEmitTest.cpp (beside :251).
Red-once. (1) Empty the applier's block-binding tail → UniformInitializerScenario /
Glsl420DeclarationScenario wrong pixels + RefusedObjectCalls. (2) Leave the blob refs at Size 0
with the companions removed → Fatal{ProtocolCorruption} (PipeApply.cpp:2858-2871). (3) Delete
the five scopes without re-keying → Fatal{RoleViolation, "MGPipeSlots"} (after ra). (4) Strict lane
names GetProgramForDraw@draw_vbo with the row still pulled; green after. (5) Force bit 12 off
under run-ahead → Fatal{UnmigratedVerb, "set_global_constants"}. Quick gate. Unit +
integration-split; RelinkStageSetScenario, PostLinkAttachScenario, ProgramPipelineScenario,
AsyncCompileScenario, CsoContentAddressingScenario, HandleRecycleScenario; the archive cost
measured (links/frame, bytes/link, ms on the GL thread) on MC + a shader pack and reported before
the package is declared done. New names: MGPipeApplySetProgramBindings, EmitProgramBindings,
SyncCurrentProgramByHandle.
tx2 — textures and samplers (S2)
Scope. DirectGLES.cpp:1723-1806 (SyncTextureToBackendByHandle), :1867-2098 (unit
epoch/capture: monolith-only; decline → refusal), :2127-2140 (UnitTextureSyncEntry re-shape,
push-only under #if), :2321-2391 (the unit half), :4834-4948 (ResolveAndBindUnitTextures),
:4985-5026, :5051-5170 (BindCurrentUnitSamplers, decline → refusal), :5245-5305
(BindCurrentTextures with ruling 6's key), :5561-5632 (the sampler pass; the mint deleted),
:8725-8760 + :9560-9600 (GenerateMipmap by VerbMipRes), :9161, 9216 (mip shape HandleOf
→ handle), trailing :9714-9735 (CopyTex endpoint) and :11884 (GetTexImage); Managers.cpp:5816-5975
(RequireImageBindableStorage entry refusal), :6902-6985, :7049-7400 (SyncMipmapsToBackend(h, rec)), :8472-8560, :8712-8900, :13184-13290 (sampler identity family deleted under
transport), :13457-13486; Managers.h:1626-1660 (IsDrawSyncCleanByRecord), :1785-1812,
:2678-2700; MG_Backend/DirectGLES/MipmapStorage.cpp:39-51 (guard list widened);
PipeApplier.cpp (the window checks at draw/dispatch dispatch — ONE function CheckUnitWindows(st)
called from OnDrawVbo/OnLaunchGrid); MG_Test/Wire/RemoteClientTest.cpp:1938-1978;
MG_Test/SanityTest.cpp:3075-3144 (+ the ABA sibling); SampledSetStalenessScenario,
TextureParamsWithoutASamplerViewScenario kept. Red-once. (1) Revert any handle arm → the
widened guard aborts by accessor name. (2) Revert the window arm of ResolveAndBindUnitTextures →
strict lane Fatal{UnmigratedPipeInput, "GetTextureUnitObject@DrawVbo"}. (3) ABA: recycle a slot to
{s, g+1} while the old entry is in the work list → the twin is re-minted; skip the Gen reset → red.
(4) Narrow SamplerEmit.h:735-737's count by one → Fatal{ProtocolCorruption, "SetSamplerViews.Count"}.
(5) GenerateMipmap with the row kWaitNone and the unit walk restored → Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@...} (F1WireScenario.GenerateMipmap*Pixels). Quick gate. Unit +
integration-split; PipeSlotPeek asserts zero registry-scope entries per frame for this family.
New names: SyncTextureToBackendByHandle, SyncMipmapsToBackendByHandle,
ResolvePushedTextureParams/BuiltinSampler(MGPipeHandle), IsDrawSyncCleanByRecord.
fb — framebuffers, attachments, images, blit (S3)
Scope. DirectGLES.cpp:179-241 (the doors), :2140-2180 (list keys), :2227-2331
(SyncReadFramebufferTextureAttachments), :2405-2470 (the draw-FBO list), :2519-2914 (images:
SyncImageTextureBinding(const MGPImageView&), ResolveShaderImageRecord deleted,
MarkWritableImageBufferTexturesGpuWritten deleted under transport, the sweep gate), :3017-3377
(SyncCurrentFBO*), :4514-4754 (BindCurrentFBO, SyncAndBind*, ForceBindCurrentFBO),
:6180-6192 (Clear), :8182-8420 (both blit arms), :8837-8935 (the detach walk + reverse index),
:9688-9740 (SyncRenderbufferObjectToBackend by handle); Managers.cpp:9395-9405, 9585-9742, 9834-9953, 10044-10429, 10847-10850, 13530-13584; Managers.h:1857, 1943-1970, 2819-2830
(reverse index texture → FBO slots); MG_Test/Pipe/FramebufferEmitTest.cpp (server twin of :558),
MG_Test/Pipe/ImageEmitTest.cpp (beside :452: the three constants on both sides), a new
default-framebuffer-resize re-emit case. Red-once. (1) SyncAttachmentSurface takes the
frontend attachment again → strict Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot"} on
SnormAttachmentScenario. (2) The draw-FBO list key back to the pointer compare → Fatal{RoleViolation, "MGPipeSlots"} (after ra). (3) MGPipeDecodeImageAccess mapped 1↔2 → FormatlessImageBakeScenario /
ImageLoadStoreSsoScenario red + the unit case. (4) The named blit back on the frontend objects →
CopyImageLayeredScenario, LayeredAttachmentShapeScenario. (5) The per-point version memo kept as
a second gate → CrossFrameBufferScenario / P4aSeamAuditScenario F-3 must stay green WITHOUT it.
Quick gate. Unit + integration-split with the framebuffer/sampler bits set; the seam greps
(DirectGLES.cpp:3086-3106, 2607-2610, 2636-2638) silent; the object A/B re-baselined for
SyncReadFramebufferTextureAttachments. New names: SyncToBackendByHandle (RBO/FBO overloads),
MarkBufferGpuWrittenByHandle (declared; body deleted under transport with sb), the reverse index.
ra — the transport mechanics (S6; parallel from day one; lands last)
Scope. MG_Remote/Client/ClientSession.{h,cpp} (RunAheadArmed, EmitAndWaitTails §2.3,
ReleaseFillPins, DrainEventRing at every park exit, Finish, RefusePipeInputsTouch... §3.5,
the stats line credit-waits); MG_Remote/Client/EmitTables.cpp:977-1053 (EmitPresent credit +
FrameSerial) and the Finish slot; MG_Remote/Client/BackendObject_Remote.cpp (wait before every
Server* EGL forwarder); MG_Remote/Client/WireTables.cpp:332-352 + MG_Pipe/PipeRoute.h:270-276
(wantReply for the buffer half); MG_Remote/Client/CapsMirror.* (latch); MG_Impl/Pipe/PipeFill.cpp:84-86, 546-556, 1955+, 2962-3306 (§3.1, §3.4); MG_Backend/MGPipe/PipeInputs.{h,cpp} (server-private
stamp, §3.2-3.3); MG_Remote/Server/PipeApplier.cpp (OnPresent credit return, StampVerbBoundary,
MGPipeBarriered body; ApplyOne's flag line is id's); MG_Remote/Server/ServerSession.{h,cpp}
(present credit, event producer flow control); MG_Remote/Server/ServerLoop.cpp:375-392 (park
predicate); MG_Remote/Transport/EventRing.h, SessionRings.h; MG_Backend/Init.cpp (flip
kMGPipeP5eRunAheadReady — the integration commit); MG_Impl/Pipe/PipeMutation.h:216-236 (the
unbarriered-enqueue finding); .github/workflows/test.yml:1140-1206 (the strict lane's allowlist
step); docs/Disaggregated/ARCHITECTURE.md §11, §14, §17; MG_Test/Wire/{RemoteClientTest, SessionTest, ServerLoopTest}.cpp. Red-once. (1) Encode present without WaitForPresentAck →
the SessionTest.cpp:522-549 twin in RemoteClientTest parks a credit-1 client behind a stalled
apply thread; without the wait the third present publishes and Waits() is unchanged → red by
count. (2) Leave one CopyField in an unbarriered fill → Fatal{RoleViolation, "gPipeInputs"} by
name (RemoteGuards...FillWhileTheApplierOwnsIt's shape with RunAhead=1). (3) Count instead of
Fatal under an unbarriered record → the strict lane's rewritten step turns from green to red on the
first pulled row. (4) Keep Fatal{EventRingOverflow} → a unit case publishing 300 KiB of
kEventGpuWritten behind an unwaited sequence aborts; with flow control it completes. (5) Publish
bit 10 for Magma → MagmaPipeIdentityTest red. (6) MOBILEGL_IPC_RUN_AHEAD=0 renders identically
(SSIM); MOBILEGL_IPC_VERB_BARRIER=0 under run-ahead is the documented red. Quick gate (before the
flip). Unit + integration-split green with the bit unpublished (every branch inert);
RemoteGuards strict arms green. New names: MGPipeBarriered, MGPipeWaitClassFor (c0e),
ClientSession::RunAheadArmed/ReleaseFillPins, MGPipeServerStampVerbBoundary's new signature,
IpcTable::RunAhead/PresentCredit.
3 The integration gate (the commit that flips kMGPipeP5eRunAheadReady)
- Unit green under both
MOBILEGL_IPC_STRICT_ERRORSarms. integration-splitgreen (111+ entries plus the new ones), broad inproc census with zero regressions vs2fde7034, the 79 traces with no new first blocker.integration-split-strictGREEN as a hard lane: the only admitted[BARRIER-PULLED, ...]markers are the §7 allowlist (<field>@<verb>for readbacks, XFB, CopyTex,set_storage_block_binding); any other marker fails the lane. Every scenario summary showsrsp= 0 on unbarriered records.- The three negative controls behave as documented:
RUN_AHEAD=0identical SSIM;VERB_BARRIER=0red at a barriered row;RUN_AHEAD=1on Magma logs once and runs lockstep. - Every package's red-once re-run on the integrated tree (a revert of any one handle arm goes red in the strict lane by field and verb).
- G1 0/0/0/0, G2, G14, G5; the push build under
Transport=monolithrenders the four A/B traces with unchanged SSIM. MOBILEGL_IPC_AUDIT=1clean (no0xDDread) with the client a present ahead — the pmap push without a reply is the new consumer of retired staging.
4 The device exit (Redmi, MC 26.3 VD12, 30 s in-world window)
Arms: monolith; inproc lockstep (MOBILEGL_IPC_RUN_AHEAD=0); run-ahead credit 1; run-ahead credit 2.
Per arm: fps; per-thread CPU ms/frame (client GL thread, mgl-srv-apply); cliwait/clipark,
credit-waits, srv/srvpark from the wire ledger and the stats line (ClientSession.cpp:1161-1189,
ServerLoop.cpp:410-414); simpleperf on both threads — the exit condition is that
WaitForApplied self on the client falls from 27.9% to the present-credit wait alone and
DrainRing's real work is unchanged; the per-op wait tally must show NO kWaitApplied row and no
reply row firing per draw on the steady path (what remains per frame — the texture subdata for the
lightmap, set_texture_params replies, fences — is listed by op in the report as the trailing
inventory); rsp = 0 on unbarriered records; SSIM of the four A/B traces vs monolith unchanged.
The P5D report's method section (P5D-INPROC-PERFORMANCE.md:58-71) is the template; the bench
scripts the brief named are not on this machine and are re-created beside it. Recorded, and
GATED on: fps ≥ lockstep's, client-thread ms/frame < lockstep's, SSIM unchanged.
5 What trails (after the integration commit, each its own small package)
| item | owner | why it can trail |
|---|---|---|
GenerateMipmap kWaitApplied → kWaitNone (if tx2 lands the VerbMipRes arm late) |
tx2 | not on the steady path |
set_storage_block_binding by ShaderCso |
pg | after-link only |
CopyTex endpoint / GetTexImage / readbacks by handle (the barriered scope sites 6166, 8837, 9161, 9216, 10029, 11884) |
tx2 / fb | barriered rows keep P5C's semantics (P8/P9 phases) |
resource_subdata texture half without a reply (D-D5 accept-by-construction) |
ra + tx2 | one wait per frame for the lightmap today |
set_texture_params without a reply (the default-texture self-heal moved client-side) |
tx2 | creation-time traffic |
client vertex arrays staged ({BindingIndex, MGHostSpan} tail) |
P8 | refused under run-ahead |
XFB migration (set_stream_output_targets, the program row, the targets) |
P3b/P4b/P9 | lockstep by escalation (i) |
ResolvedDrawBuffers deletion |
vi | measurement first |
composite pipelines' uniform mirror vs set_program_bindings fire rate |
pg | unmeasured; pipeline workloads only |
program-derived set_shader_buffers window |
sb | narrowing, not correctness |
MOBILEGL_IPC_EVENT_KB (ring size under SSBO-heavy kEventGpuWritten) |
ra | Iris-class workloads |
Flush as the doorbell point (ARCHITECTURE §11.6); credit-2 latency study |
ra | measurement |
| thread placement | — | out of scope by instruction; the per-thread numbers survive it |
6 The rulings the integrator makes (conflicts between scout designs, with the resolution)
| # | conflict | resolution (contract §9) |
|---|---|---|
| 1 | S7: handle arms run under Transport=monolith too (family bit selects); S1–S5: Transport != Monolith keeps the frontend arms under monolith |
S1–S5's shape. The push-monolith build keeps its bytes and behaviour, the verify comparator keeps its frontend arm, and RUN_AHEAD=0 becomes a pure wait-rule A/B on identical server code. |
| 2 | Brief/S7 assumed client vertex arrays are refused under split; S1 read the code: they are not (EmitTables.cpp:651-685) |
Verified S1 is right. Refuse under RunAheadArmed with kDrawClientArrays; lockstep unchanged; staging is P8's. |
| 3 | S5: XFB/atomic waits decided from client state at emit; S6: a static per-op wait table | The wire-derived predicate MGPipeBarriered(op, payload, st) — static column + XFB-active from MGPContextValues + the draw flag — computed identically on both sides. |
| 4 | S6 §4(6): a WAITED op may no longer read the frontend; S1–S5 keep readback/CopyTex/GetTexImage sites barriered | S6's argument is wrong for a single-context client parked in its own wait; barriered records keep P5C's semantics (rule F is scoped to unbarriered records). This is what keeps the first landing small. |
| 5 | S4 §4.6: ShaderStorageBlockBinding is a GL-thread entry; S6: set_storage_block_binding is a kCtxVerb applied on the apply thread through GetProgramObject(GlName) |
Verified S6 (PipeCalls.def:243, PipeApplier.cpp:805-808). kWaitApplied now; pg's trailing item resolves by ShaderCso. |
| 6 | S2: BindCurrentTextures memo on SamplerViewsSerial alone; S4: on the program handle + serials |
Both (a relink with unchanged resolution still needs the twin's new unit assignments). |
| 7 | S3 R1: the image sweep gate needs S2's serial design | (ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration); keep the union walk. |
| 8 | S4 R1: encode the archive per link vs pin a SharedPtr |
Encode (rule B); measure; lazy encode at first bind as the fallback. |
| 9 | S4 R2: a failed relink of a bound program | Re-issue with LinkStatus = 0 iff the frontend reports it unlinked; pg verifies the frontend's actual behaviour; never object_death. |
| 10 | S5 R2: high-water vs program-derived window | High-water (TouchedBufferBindingPointCount, already carried); 84 on the wire, backend clamps. |
| 11 | S5 R1: one suppressor slot for three classes | Three slots. |
| 12 | S7 R1: Magma's four apply-thread allocator sites vs an unconditional guard | One renamed scope, backend-kind-keyed; Magma is lockstep and every record is barriered there. |
| 13 | S6 §3(9)/S7: the deferred-destroy queue "loses its producer" | It does not (barriered fills pin objects); the queue stays live; an unbarriered enqueue is the finding. |
| 14 | S6 §3(3)/S2: GenerateMipmap waits until the unit read is gone |
Verified (ClientSession.cpp:900-914; VerbMipRes already exists at DirectGLES.cpp:8736). tx2 flips the row. |
| 15 | S3 R5: does an EGL resize re-emit the default-framebuffer record | Verified yes (ClientSession.cpp:210-221 → TextureObject.cpp:84-102 → Tracker.h:594); fb pins it. |
| 16 | S3 vs the brief: Access "has no encoding" |
Verified S3: MGPipeEncodeImageAccess exists (ImageEmit.h:163-176); the server comment is stale; the decode moves to MGPipeValueTypes.h. |
| 17 | S6 §7: resource_subdata's texture half |
Buffer half kWaitNone in ra; texture half keeps its reply; trailing. |
| 18 | present credit default | 1. |
| 19 | S1 R4 / S2 R1 (A8): the var-tail windows are not pinned | The window rule in §5.3 with the sink check at every draw/dispatch; set_vertex_buffers' window is pinned by a test in vi (same class, same validate step). |
Open items that need a ruling the scouts could not settle and this brief does not: (a) whether
DoublePrecisionScenario uses client arrays under split (decides whether vi's refusal needs a new
scenario as its red-once); (b) the archive cost number (ruling 8's fallback trigger); (c) the NoSession
server-only fixtures that lose death delivery when id deletes the hop (grep RunOnApplyThread MG_Test/Wire) — id enumerates them in its first commit and the integrator rules keep/rewrite.