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.
25 KiB
S6 - transport mechanics of run-ahead (head 2fde7034, paths under MobileGL/)
1. The exact sync points of the transport family today (live = a wait or a cross-role write that exists only because of the lockstep; rec = already safe)
| site | what it does | kind |
|---|---|---|
MG_Remote/Client/ClientSession.cpp:840-846 |
Fatal{BarrierViolation} if the apply thread is inside the applier at emit - only when BatchWaits==0; dead in the default config |
live (guard) |
:848-872 |
EncodeRecord; on kInvalidSeq WaitForCmdSpace(needed, 30 s) -> Fatal{RetirementWaitFailed,"SEG_CMD"} |
rec (the only byte backpressure) |
:877 PublishAndNotify(seq) |
publish -> submittedSeq -> ring (Ring.cpp:437-462) | rec |
:883-884 |
ownsReplySlot from kMGPipeCallFlags (14 rows) |
rec |
:915-922 |
the batch skip: kCtxState/kCtxCso/kCtxObject without a reply, and not GenerateMipmap, return without waiting |
rec |
:924-930 |
VerbBarrier==0 negative control: nothing but reply rows waits |
live (control) |
:932-959 |
WaitForAppliedBudget(seq) - every kCtxVerb (18 rows, PipeCalls.def:200-236), every kScreen (12) and kCtxQuery (8) row, every reply row, GenerateMipmap |
live: R-1 |
:966 DrainEventRing(m_events) |
the ONLY steady-state drain of SEG_EVENT, inside the wait ("the one instant the apply thread is known not to be inside the applier", :961-965) | live |
:968-998 |
the reply read; Fatal{ReplyMissing} if the slot does not stamp seq |
rec |
:1015-1056 RefusePipeInputsTouchWhileApplierOwnsIt |
returns at :1037 whenever BatchWaits!=0: the layer-2 gPipeInputs guard is disarmed by default |
live (guard, dead) |
MG_Impl/Pipe/PipeFill.cpp:2962-3306 MGPipeValidateForVerb |
client WRITES into gPipeInputs: :2989 ++CurrentVerbSerial, :2997 MGPipeServerClearVerbBoundary() (withdraws the SERVER's stamp from the GL thread), :2999-3001 SetVerb/SetIdentity, :3252-3270 step 4 CopyField + FilledGen[i] stamps; :3304 verify EntryCompare |
live: the block is shared |
PipeFill.cpp:84-86 VertexAttribDefaultsOf |
client READ of gPipeInputs.m_currentVertexAttribute (what the applier left) to diff EmitVertexAttribDefaults |
live: client reads server memory |
PipeFill.cpp:546-556 MGPipeNoteFrontendMutation, :1955+ MGPipeLeaveVerb |
further client writes into the block | live |
PipeFill.cpp:815-839 -> MG_Pipe/PipeRoute.h:270-276 -> Client/WireTables.cpp:332-352 |
resource_subdata carries kReplySlot (PipeCalls.def:188): every record EmitAndWaits; the Bool is discarded at PipeFill.cpp:839. This is the persistent-map push path (PersistentMapTracker.cpp:735,:850 PushMappedSpanBlock -> BufferObject.cpp:142) - one lockstep round trip per 64 KB block, answer never read |
live, hidden |
Client/EmitTables.cpp:222-234 BeforeDrawVerb/BeforeReadOnlyVerb |
pmap push, deferred-destroy drain, GPU-write marks, before the verb record | rec |
EmitTables.cpp:977-1053 EmitPresent |
FrameSerial = 0 (:1040, "the server stamps its own"), EmitAndWait(Present) waits as a kCtxVerb; PumpControlPlane :1049 |
live: present is a full barrier |
Server/PipeApplier.cpp:269-293 OnPresent |
table->Present(); m_lastPresentSerial; never calls ServerSession::ReturnPresentCredit (ServerSession.cpp:677-682 has zero production callers; WaitForPresentAck Ring.cpp:495-502 is exercised only by SessionTest.cpp:522-549) |
gap |
PipeApplier.cpp:1043-1082 ApplyOne |
ScopedApplierEntry :1057, StampVerbBoundary :1061 -> MG_Backend/MGPipe/PipeInputs.cpp:155-184 (writes m_filled, m_currentVerb, m_serverStampedVerb), LeaveApplier :1080 -> :186 |
rec on the server side; races the client rows above |
PipeInputs.cpp:191-224 MGPipeInputUnfreshRead |
BARRIER_PULLED read in class -> CountBarrierPull :71-77: rsp++, Fatal{UnmigratedPipeInput,...}[BARRIER-PULLED, STRICT] only under StrictErrors; otherwise the read is SERVED from the client's last fill |
live: the non-strict arm serves a torn value under run-ahead |
Transport/SessionRings.h:410-451, Server/ServerLoop.cpp:559-631 |
appliedSeq +1 per record; RetireThrough once per drain batch (:624); the park predicate :380-383 |
rec |
MG_State/.../BufferObject.cpp:571-651 SyncGpuWrites -> Client/GpuWritePending.cpp:202-228 AwaitBufferWriteback |
readback = reply row -> waits -> the drain at :966 lands the writeback; Fatal{UnimplementedWritebackWait} if still pending |
rec (stays a wait) |
Client/BackendObject_Remote.cpp:200,208,216,245 |
the four RPC-side DrainPublishedEvents (surface create/resize/pbuffer, make-current) |
rec |
ClientSession.cpp:691-786 Stop |
LastPublishedSeq :739, WaitForApplied(submitted, 5 s) :742, Shutdown :755, ServerLoop::Stop :760, free after the join |
rec |
MG_Pipe/PipeMutation.h:216-236, PipeFill.cpp:984+ |
deferred destroys: an apply-thread last-owner death is queued, replayed at the next verb hook | rec (belt) |
Server/ServerSession.cpp:174-181,:183-254,:634-666 |
SEG_EVENT producers: lossless, Fatal{EventRingOverflow} on a full 256 KiB ring (SessionRings.h:96) - "a full ring under lockstep is a burst no workload has" |
live: premise dies with the lockstep |
MG_Impl/Pipe/SlotAllocator.cpp:22-38 |
MGPipeRefuseAllocatorFromApplyThread exempts the two scopes at :28-29 |
live (the exemption) |
ConfigLoader.cpp:377 |
PipeVerify -> BatchWaits = 0: the verify build forces the per-record barrier (confirmed) |
rec |
Sizes for §3(2): DrawVbo = 8 B header (Ring.h:164) + 56 (MGPDrawInfo, MGPipeTypes.h:1441) + 12 per range (:1448) -> 80 B padded; Present 16 B; resource_subdata 72 B + its SEG_STAGE run.
2. What the transport already carries for run-ahead
- Five watermarks on
RingControl(Ring.h:138-139completedFrameSerial,presentAckSerial);SessionProducer::WaitForPresentAck(Ring.cpp:495-502, bounded,ShutDownresult),SessionConsumer::ReturnPresentCredit(Ring.cpp:607-613, advances AND rings),ServerSession::ReturnPresentCredit(:677-682),MGPPresent::FrameSerial(client sends 0),m_lastPresentSerialon the sink. - The class/flag tables at the emit site (
MGPipeCallClassFor,MGPipeCallFlagsFor,ClientSession.cpp:884,:917) - the wait rule is already table-driven for one class. SEG_CMDbackpressure (WaitForCmdSpace),SEG_STAGEretirement onretiredSeq(Wire/PipeWireCodec.cpp:826-842, 5 s bell wait;ReclaimStagedBytes:1126-1163 is already written for "pipelining deeper than fully drained", :1138-1144).EventRingConsumer::RingIsFull()/Drained()clear theeventRingFulllatch (Transport/EventRing.h:185-212); the producer latches it at:135-143; ARCHITECTURE.md §11.7 (:419) already specifies the two-sided protocol (server stops applying at a record boundary, rings; client drains, clears, rings).- The server stamp's constexpr answerable mask (PipeInputs.cpp:104-141) and the strict detector (
:61-67);MGPipeServerClearVerbBoundary;ScopedApplierEntry(ClientSession.h:155-160);InBarrierWait/ApplyThreadIsInsideApplier(:1010-1013). CapsMirror::HasCap(Client/CapsMirror.h:71-72) overMGPCaps::CallMask; bits 0..9 used (MGPipeTypes.h:108-135); the server sets the mask by backend type (ARCHITECTURE §17.2).IpcTable(Config.h:465-518) +InitIpc(ConfigLoader.cpp:356-403);PRESENT_CREDITis already in the planned inventory (Config.h:462, ARCHITECTURE.md:469 "default 1", :619).- Deferred-destroy queue,
object_death(kCtxObject - already batched),applier_reset(kScreen - waits),DrainPublishedEvents, the wait ledger (Waits()/Parks()).
3. The gap - knobs, fields, records, rules that do not exist
IpcTable::RunAhead=MOBILEGL_IPC_RUN_AHEAD(default 1; forced 0 byPipeVerifybeside:377; meaningless whenVerbBarrier==0).IpcTable::PresentCredit=MOBILEGL_IPC_PRESENT_CREDIT(default 1, range 1..8 - see §4(2)).kCapRunAheadApply = 1ull << 10inMGPCapBit; set in the DirectGLES arm of the server'sCallMask, never in the Magma arm. Client predicateClientSession::RunAheadArmed()=m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply), latched at the first CapsSnapshot adoption afterStart(a later snapshot may only turn it OFF - a caps re-run cannot make a lockstep server run-ahead mid-frame).- A wait class per op, generated beside
kMGPipeCallFlags(MGPipeWaitClassFor(op)):kWaitReply(everykReplySlotrow),kWaitApplied(ApplierReset,FenceWaitServer? - no: GPU-side, fire-and-forget; onlyApplierResetandGenerateMipmapuntil S2 retires theGetActiveTextureUnitread at DirectGLESGenerateMipmap, ClientSession.cpp:900-914),kWaitPresent(Present),kWaitNone(everything else, includingResourceDestroy,UnmapPersistent,FenceCreate/Destroy, the five non-reply query rows, allkCtxVerbrows butReadPixels/Present). Row-level override, not a per-op runtime guess. resource_subdatawithout a reply for the buffer half. The row keepskReplySloton the wire (the server posts; an unread slot is harmless: the client reads a slot only afterappliedSeq >= seq, and a slot is rewritten only by a record 8 seqs later, ReplySlot.h:16,:105);Wire_ResourceSubData(WireTables.cpp:332-352) takes awantReplyfrom the route:MGPipeRouteResourceSubDatapassesnullptrfor buffers (the Bool at PipeFill.cpp:839 is discarded today) and the texture drain's D-D5 acceptance is a ruling for S2/S8 (see §7). Without this the pmap push stays lockstep.MGPPresent::FrameSerialminted by the client (++m_presentsSent, 1-based);OnPresentcallsServerSession::Active()->ReturnPresentCredit(present.FrameSerial)aftertable->Present()returns (m-1's 1:1 holds: one credit per swap). Client:WaitForPresentAck(m_presentsSent - PresentCredit)BEFORE encoding present N whenm_presentsSent - PresentCredit >= 1.gPipeInputsclient-side skip. InMGPipeValidateForVerb, whenRunAheadArmed(): skip:2989,:2997,:2999-3001, the whole of step 4 (:3252-3270), andMGPipeLeaveVerb's writes;MGPipeNoteFrontendMutationreturns at:554; step 2 (tracker walk) and step 3 (emitters) run unchanged.VertexAttribDefaultsOf(:84) must diff against the emitter's own last header (g_attribDefaultLastHeader, :2949) - a client mirror, never the block. One-time release of the four O-class SharedPtr rows (PipeInputs.h:801-809m_boundVertexArray, the three programs) at the switch, on the GL thread, or the last lockstep fill pins those objects for the process life and the apply thread can become their last owner.SetIdentity(:3001) moves to the server (ApplyOnesets it once from its own backend).- Server stamp under run-ahead.
MGPipeServerStampVerbBoundary(verb, runAhead);MGPipeInputUnfreshReadunderrunAheadtakesStrictBarrierPullFatalregardless ofStrictErrors(the value in the row is torn or stale by construction, P5e-3);CountBarrierPullstays for lockstep. The sticky forwards (MGPipeStickyForwardPull:251-257) are the same: Fatal under a run-ahead verb. - Event-ring flow control (ARCHITECTURE §11.7 made real): server
Reserve==nullptr-> latcheventRingFull(exists), publish,NotifyClient, then park inServerLoop::ApplyThreadMain's loop witheventRingFull==0added toready(:380-383) - NOTFatal{EventRingOverflow}whenRunAheadArmed; client: everyParkexit inClientSession(applied, present-ack, cmd-space, and the stage bell at PipeWireCodec.cpp:836, which is the sameSelfDoorbell) re-checksm_events.RingIsFull()and drains before re-parking. Deadlock argument: the server blocks only on the event ring; the client blocks only on watermarks the server advances; the client's every wait drains; so at most one side is parked at any instant. - Deferred destroy under run-ahead: keep the queue;
MGPipeDeferDestroyAndFreeIfOnApplyThreadunderRunAheadArmedlogsMGLOG_E_ONCEwith the kind (after S1-S7 the server holds no frontend SharedPtr; an enqueue is a finding) - Fatal in strict. RefusePipeInputsTouchWhileApplierOwnsItis re-derived: under run-ahead the rule is "the GL thread never touches the block", so the guard becomes unconditional on the surfaces named in (6) whenRunAheadArmed()(theBatchWaitsearly return at:1037goes; the check no longer consultsApplyThreadIsInsideApplier).Fatal{BarrierViolation}(:840) stays forBatchWaits==0 && !RunAhead.
4. The retirement design
(1) The wait rule. EmitAndWaitTails after PublishAndNotify:
if (ownsReplySlot) -> WaitForAppliedBudget + DrainEventRing + ReadReply (unchanged)
else if (RunAheadArmed()) switch (MGPipeWaitClassFor(op)) {
kWaitNone: return seq;
kWaitApplied: WaitForAppliedBudget + DrainEventRing; return seq; // applier_reset, generate_mipmap (until S2)
kWaitPresent: (credit wait happens in EmitPresent BEFORE encode; here: return seq) }
else -> today's :915-959 shape, byte for byte
VerbBarrier==0 keeps its meaning (nothing but reply rows waits) and stays the R-1 negative control for the lockstep arm; under run-ahead its expected red is applier_reset ordering (Fatal{ProtocolCorruption,"ApplierReset.ContextSerial"} cannot fire - the ring orders it - so the control has to be re-pointed at the strict detector: §6). Magma: kCapRunAheadApply absent -> RunAheadArmed()==false -> today's path; the negative control for the switch is MOBILEGL_IPC_RUN_AHEAD=1 on Magma, which must log "run-ahead requested, server does not publish kCapRunAheadApply" and run lockstep.
(2) Present pacing. EmitPresent: BeforeReadOnlyVerb(); if (m_presentsSent >= credit) WaitForPresentAck(m_presentsSent + 1 - credit, 30 s) [ShutDown -> return; TimedOut -> Fatal{PresentCreditTimeout}]; DrainEventRing; encode/publish; PumpControlPlane. With credit 1 the client publishes frame N+1's records while the server applies and swaps frame N and blocks at present N+1 until swap N returned - one full frame of overlap, the shape ARCHITECTURE §14 defaults to; credit 2 adds one frame of end-to-end latency for no CPU gain on a CPU-bound client. Default 1. Ring budget at ~850 draws + ~0.36 MB pmap/frame: SEG_CMD in flight per frame is ~68 KB of draws + the set/CSO records (bounded by the same frame) - well under 1 MiB, so 8 MiB holds 8+ frames; SEG_STAGE at 0.36 MB/frame + subdata/texture blobs holds 32 MiB / ~1 MB = ~30 frames; SEG_REPLY unchanged (reply rows still one deep). Backpressure is therefore the present credit, never the bytes; WaitForCmdSpace/the stage bell are the safety net and both must drain events on exit (§3(8)).
(3) Drains and forced waits. Drain points: every wait exit (reply rows, present credit, cmd-space, stage), the four RPC sites, and Stop. Client operations that force a wait: MapBuffer(READ) / GetBufferSubData / CopyBufferSubData source -> SyncGpuWrites (reply row, unchanged); glFinish -> emits Flush? No: Flush/Finish stay no-ops on the wire (ARCHITECTURE:415); glFinish under run-ahead = WaitForApplied(LastPublishedSeq) + drain, spelled in the client Finish slot (new, one line); glClientWaitSync / GetSyncStatus / QueryAvailable / QueryResult are reply rows (wait); glGetError: the relaxation - server-side GL errors (kEventGlError, DirectGLES.cpp:8804 -> ServerSession.cpp:634-666) are observed at the next drain, so glGetError reports a run-ahead verb's error at or after the next wait point rather than at the call after it; CONTRACT-P5C §4.2 already assigns the ordering to P9; P5e names the window as "<= one present credit". eglMakeCurrent / surface RPCs: RunOnApplyThread already serialises with the ring? NO - the mailbox is pumped between DrainRing batches (ServerLoop.cpp:385-390), so a make-current can land between two run-ahead records. Rule: the client waits WaitForApplied(LastPublishedSeq) before every Server* EGL forwarder (BackendObject_Remote.cpp) - the "control records that need it" of P5e-2; applier_reset is the same, it is a kWaitApplied row.
(4) gPipeInputs ownership. Under a live wire and RunAheadArmed() the block is server-role memory: the client executes steps 2 and 3 of MGPipeValidateForVerb and nothing else; the server stamps at ApplyOne (unchanged), SetIdentity once per session; the detector is (§3(7)); the verify build forces RunAhead=0 (comparator needs a client-filled block, PipeFill.cpp:3304). The residual-value trip wire (:3284-3294) still emits - it compares on the SERVER against the applier-derived mirror, which is the honest half. The MGP_FILL call sites (GL_Drawing.cpp:540-640) do not change.
(5) Object lifetime. ABA: the client frees a slot right after emitting the destroy (PipeFill.cpp:979-980) and the next Acquire bumps Gen; every record naming {slot,gen} precedes destroy{slot,gen} on the ring and every record naming {slot,gen+1} follows create{slot,gen+1}; the in-order ring plus a Gen compare in every server twin table (S7's rule) closes it - no drain wait is needed at delete. object_death is already batched; resource_destroy becomes kWaitNone. The published latch is keyed {kind,slot,gen} (PipeMutation.h:294-310). Persistent maps: Forget() (PersistentMapTracker.cpp:599-606) drops tracker state only; the pushed bytes were COPIED into SEG_STAGE at StageOptional (WireTables.cpp:342) and reclaim waits on retiredSeq, so a map released while blocks are in flight is safe; a glBufferData respecify after in-flight subdata is ordered by the ring. Client memos holding frontend->handle maps (MGPipeResourceTracker::Find, g_fenceProxies, the emitters' latches) are GL-thread-only and untouched. Fence: EmitDeleteSync frees the Fence slot after emitting (EmitTables.cpp:1507-1519), same ABA answer.
(6) The two scopes. After S7 the guard at SlotAllocator.cpp:22-38 loses :28-29: MGPipeSlots() from the apply thread is unconditionally Fatal{RoleViolation,"MGPipeSlots"}. The 36 sites (DirectGLES.cpp 20, Managers.cpp 15, ResourceTracker.h:723) by owner: draw-path (S1 1609, S3 2588/3303/4585, S4 4236/4329/5328/5689, S2 1734/4926/4993, S5 Managers.cpp:2938 HandleOfBuffer) - retired by their designs. Not covered by S1-S5, and each is either a run-ahead op or a waited op: DrawArrays/MultiDrawArrays client-side attribute arms (6729/6775, run-ahead ops - refused under split already? S1 confirms; if refused the scope is dead code); GetBackendProgramId (6166) and ShaderStorageBlockBinding (10029) - set_storage_block_binding is a run-ahead kCtxVerb that resolves the program via GetProgramObject(GlName) (PipeApplier.cpp:805-808): must resolve through MGPStorageBlockBinding::ShaderCso (carried, unread) -> S4's twin; ScopedCompleteFramebufferBinding (8837, texture Find inside blit/readback helpers) and the mipmap generators (9161/9216 HandleOf(texture)) - GenerateMipmap and CopyFramebufferToTexture are run-ahead ops whose handle already rides the record (VerbMipRes, VerbCopyTexDst); the probe must go by-handle; SyncRenderbufferObjectToBackend (9695) - renderbuffers have a create/respecify handle (P4a) -> FindByHandle; GetTexImage (11884) - a reply row (get_texture_image), waited, but still an apply-thread probe: by-handle via the record's Res; Managers.cpp 256 (OnFrontendStateObjectDestroyed, the mailbox arm - dies with the mailbox), 4544/5882/7087/8481/8717/9256/9310/9709/10382/10403/12949/13273/13570 (sampler view, texture id, texture view, FBO InvalidateSyncedState, draw-buffers, shader compile buffer-texture, sampler ctor, renderbuffer bind) - S7's re-key list; ResourceTracker.h:723 (MGPipeAnnounceBufferGpuWritten, Magma) - lockstep only, stays behind !RunAheadArmed. Readback paths (resource_readback, read_pixels, get_texture_image) are WAITED ops but the wait no longer makes a frontend read legal (the client runs on after publishing everything before it): they resolve by handle too.
(7) Measurement plan (Redmi, MC 26.3 VD12). Arms: monolith; inproc lockstep (MOBILEGL_IPC_RUN_AHEAD=0); inproc run-ahead credit 1; credit 2. Per arm: 30 s in-world window, fps, per-thread CPU ms/frame (client GL thread, mgl-srv-apply), cliwait/clipark and srv/srvpark from the wire ledger and the stats line (ClientSession.cpp:1161-1189; ServerLoop.cpp:410-414), simpleperf on both threads (target: WaitForApplied self on the client falls from 27.9% toward the present-credit wait only; DrainRing real work unchanged), rsp = 0 in every arm's summary, SSIM of the four A/B traces against monolith. The bench scripts named in the brief (~/w7/notes/tools/p5d_bench_{fcl4,all4}.sh) are not on this machine; the report's §"数字" method (P5D-INPROC-PERFORMANCE.md:58-71) is the template.
5. Monolith / G1
Every change above is #if MOBILEGL_BUILD_DISAGGREGATED and behind RunAheadArmed(), which is false with no session: Transport=monolith never constructs a ClientSession, so the emit tables are the monolith adapters and none of EmitAndWaitTails, EmitPresent, the stamp or the guards runs; MGPipeValidateForVerb's skip is a runtime predicate inside the existing MOBILEGL_BUILD_DISAGGREGATED block (:2964-2972 shape) - the pull build's bytes do not move. The one server-side file with a monolith arm, PipeInputs.cpp, is compiled only under MOBILEGL_PIPE_PUSH and its new runAhead argument defaults false. PipeCalls.def does not change (no new opcode: the wait class is a generated table, the caps bit is a value).
6. Red-once
- Revert the present-credit wait (encode present without
WaitForPresentAck) ->SessionTesttwin of:522-549inRemoteClientTest: a client with credit 1 and a stalled apply thread must park; without the wait the third present publishes and the case readsWaits()unchanged -> red by count. - Revert §3(6) (leave one
CopyFieldin the run-ahead fill) -> the layer-2 guard firesFatal{RoleViolation,"gPipeInputs"}by name (RemoteGuards.ClientPipeInputsFillWhileTheApplierOwnsItIsFatalByName's shape, RemoteClientTest.cpp:1989 withRunAhead=1). - Revert §3(7) (count instead of Fatal under a run-ahead verb) -> the
integration-split-strictlane (test.yml:1140-1206) turns from expected-red into the hard green gate: rewrite its step so GREEN is required for the migrated verbs and any[BARRIER-PULLED, ...]marker is red; a revert of any S1-S5 handle arm then goes red HERE, not in a picture. - Revert §3(8) (keep
Fatal{EventRingOverflow}) -> a unit case that publishes 300 KiB ofkEventGpuWrittenbehind an unwaited run-ahead sequence aborts by name; with flow control it completes. - Revert §3(2) (publish the caps bit for Magma) ->
MagmaPipeIdentityTest/ServerLoopTest: a Magma-typed server'sCallMaskmust not carry bit 10. MOBILEGL_IPC_RUN_AHEAD=0is the A/B control and must render identically (SSIM);MOBILEGL_IPC_VERB_BARRIER=0stays the lockstep arm's negative control.
7. Risks and unsettled
resource_subdata's texture-half acceptance (D-D5):DrainTextureSubDataclears a level's dirty flag on an accepted reply. Making the rowkWaitNonerequires "accept by construction / decline is Fatal" on the server, or the pmap push keeps a wait per block. Needs S2 + integrator ruling; without it run-ahead only covers draws and the pmap path stays ~6 round trips/frame.GenerateMipmapwaits until S2 retires theGetActiveTextureUnitread;set_storage_block_bindingandresource_copy_region(GetTextureObject(GlName)sticky, PipeApplier.cpp:742-743) arekCtxVerbrows that still read client state - either wait (kWaitApplied) or S2/S4 migrate them; list for S8.- GPU-write marks are client-side and conservative (GpuWritePending.cpp:141-147);
OnGpuWrittenevents from the server (MarkBufferGpuWritten) now arrive late - the client's own marks already cover the draw-time set, so late narrowing is harmless; confirm no server-only producer exists for XFB (EndStreamOutputis client-marked, :164-167). - EGL RPC vs run-ahead (§4(3)): the wait-before-forwarder rule adds one full drain per make-current/surface event; acceptable (startup-time), but
ServerSwapEGLBuffersis NOT on that list (present is the swap). - Event-ring capacity (256 KiB): a writeback slice is a quarter ring (GpuWritePending.cpp:187-200) and the readback row waits, so flow control is the only new requirement; consider
MOBILEGL_IPC_EVENT_KBif the per-drawkEventGpuWrittentraffic under SSBO workloads (Iris) trips the full latch every frame. - The
VerbBarrier==0control's meaning after P5e; and whetherFlush(kCtxVerb, emitted?) should become the doorbell point of ARCHITECTURE §11.6 - not settled here. - Thread placement (P5D report §"什么没完成" 2) is out of scope by the user's instruction; the measurement plan reports per-thread ms/frame so the comparison survives it.