Files
MobileGL/docs/Disaggregated/notes/p5e/CONTRACT-P5E-draft.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

47 KiB
Raw Blame History

CONTRACT-P5E (draft) — the client runs ahead of apply on the Espryt draw path

Authority: this file, beside CONTRACT-P5.md (table 0, byte carriers, field ownership, R-1…R-17), CONTRACT-P5B.md (the class-C slots) and CONTRACT-P5C.md (rule E, the two named exemptions, SEG_EVENT, the guards). Where it disagrees with any of them this file is newer and wins; §8 lists every such place. Base: feat/disaggregated @ 2fde7034 (code head 1f8de61b). Every file:line was read at that commit; paths are under MobileGL/ unless they start with docs/.

How to change it. Package c0e's file, edited by the integrator first. A P5e package (id / vi / sb / pg / tx2 / fb / ra) that needs a row changed goes through the integrator; the packages compile against the rows below from day one.


§0 What P5e is, and the rule above every row

P5d made inproc CPU-bound on the client thread with the frame still client work + (per draw) wait for apply + handoff (R-1, MG_Remote/Client/ClientSession.cpp:932-959). The wait exists because the apply of a draw still dereferences client-owned memory: the object-class BARRIER_PULLED rows of MG_Pipe/FieldOwnership.def:72-106 and the frontend-keyed twin registries (MG_Backend/DirectGLES/SlotTables.h:411-449) inside the two P5C §3.1 exemption scopes. P5e retires the lockstep for every op on the Espryt draw path: the client publishes and moves on; the server resolves every object from a handle a record carried, and the only client memory it may read is the residual fill of a record the client is provably blocked behind.

Rules A–E bind unchanged. P5e adds one definition and one rule:

Definition — a BARRIERED record. A record is barriered iff MGPipeBarriered(op, payload, applierState) is true, a pure function of wire-visible data computed identically by the emit table and by the sink (§2.1). The client blocks in WaitForApplied(seq) after publishing a barriered record; it never blocks after an unbarriered one.

Rule F — an unbarriered record's apply names no client memory. With a live wire on a run-ahead server (§6), the apply of an unbarriered record reads NOTHING from client-owned memory: no frontend object dereference, no gPipeInputs BARRIER_PULLED row, no MGPipeSlots() probe or mint, no SharedPtr to a frontend object taken or held. Every object is resolved from a handle the record or the applier state carries, through a handle-keyed twin table (§4). A violation is Fatal{UnmigratedPipeInput, "<field>@<verb>"} (a pulled row, §3.3), Fatal{RoleViolation, "MGPipeSlots"} (an allocator probe, §4.4) or Fatal{RoleViolation, "<surface>"} (P5C §6 layer 1), regardless of MOBILEGL_IPC_STRICT_ERRORS — the value would be torn or stale by construction, so there is no "count it" arm. A barriered record's apply keeps P5C's rules exactly: its pulls count into rsp, its probes are legal inside a scope, and the client's wait is what makes them legal.

Refusal vocabulary is P5C's, plus: Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"} (§5.1), Fatal{UnmigratedVerb, "set_global_constants"} (§5.5), Fatal{UnmigratedEmulation, "image-bindable-redirty"} (§5.2), Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count" / "SetShaderBuffers.Class" / "SetShaderBuffers.Count" / "SetProgramBindings.<field>" / "Present.FrameSerial" / "ImageView.Access" (§1, §5), Fatal{PresentCreditTimeout} (§2.4), Fatal{RoleViolation, "gPipeInputs"} (§3.5). Fatal{EventRingOverflow} is RETIRED on a run-ahead server (§2.6).


§1 Table 0 additions — encodings the P5e rows introduce

field the ruling zero means who reads it
MGPCapBit::kCapRunAheadApply = 1ull << 10 (MGPipeTypes.h:108-135, bits 0..9 used) Set in the DirectGLES arm of the server's CallMask (MG_Backend/Init.cpp), NEVER in the DirectVulkan arm. It says "this server applies unbarriered records without reading client memory". The client latches RunAheadArmed() = m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply) at the first caps adoption after Start; a later snapshot may only turn it OFF. absent = lockstep (Magma; and Espryt until the integration commit flips kMGPipeP5eRunAheadReady, §6.2) ClientSession::RunAheadArmed, MGPipeBarriered on both sides
MGPipeWaitClass — a fourth column of PipeCalls.def's X-macro, generated beside kMGPipeCallFlags into PipeWire.inc as MGPipeWaitClassFor(op) kWaitReply (every kReplySlot row), kWaitPresent (Present), kWaitApplied (the static barriered rows of §2.2), kWaitNone (everything else). Opcode order and the flag column are untouched: the column is a value, not a new op. — EmitAndWaitTails, ServerVerbSink::ApplyOne
MGPDrawInfo::Flags gains kDrawClientArrays (next free bit beside kDrawHasUserIndices, MGPipeTypes.h:1430) Set by the client emit arm when any ENABLED attribute of the bound VAO has no buffer. §2.1 escalation (ii). no client array in this draw MGPipeBarriered; EmitDrawRecord's refusal (§5.1)
MGPPresent::FrameSerial Minted by the CLIENT, ++m_presentsSent, 1-based — no longer 0 (EmitTables.cpp:1040). The sink calls ServerSession::ReturnPresentCredit(FrameSerial) after table->Present() returns (today ReturnPresentCredit has no production caller, ServerSession.cpp:677-682), one credit per swap. 0 arriving on a run-ahead server is Fatal{ProtocolCorruption, "Present.FrameSerial"} OnPresent, WaitForPresentAck
MGPShaderBuffers + MGPBufferRange[] (MGPipeTypes.h:901-919, wire op 38, catalogued since P4a, no route/sink/emitter today) One record per Class ∈ {Uniform=0, ShaderStorage=1, AtomicCounter=2}; Start = 0 always; Count = TouchedBufferBindingPointCount[target] (the rv value already carried) clamped to kMGPipeMaxBufferBindingPoints = 84 (pinned against BufferState.h:29 in PipeFill.cpp); per entry Res = the buffer handle or null, Offset/Size from the point's range, Size = kMGPipeWholeBuffer (MGPipeTypes.h:406) for a base binding so the server re-resolves the extent against ITS descriptor at use (a glBufferData between emit and apply then binds the new extent, as GL does); WritableMask bit per entry for classes 1/2. HostSpanCount = 0 on Espryt for the whole of P5e (kCapNeedsHostUboBytes is 0, CONTRACT-P5.md:64); the codec's honesty pass (PipeWireCodec.cpp:1783-1799) is the guard. Class >= 3 → Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}; Start + Count > 84 → "SetShaderBuffers.Count". XFB targets do NOT ride this record (set_stream_output_targets keeps its row, still unemitted: XFB stays lockstep, §5.7). Res null = nothing bound at that point MGPipeApplySetShaderBuffers, SyncBufferBindingPointsByRecord, the UBO loop, SyncAtomicCounterBuffers
set_program_bindings — opcode 80, appended. X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan) Head MGPProgramBindings { MGPipeHandle Cso; Uint64 Signature; Uint32 BlockBindingCount, SamplerUnitCount, StorageOverrideCount, Pad; } (32 B, MGP_ASSERT_POD); three tails in order: Int32 BlockBindings[BlockBindingCount] (dense, GL uniform-block index order), {Uint32 Location; Int32 Unit;}[SamplerUnitCount] (sparse, ascending Location), {MGHostSpan Name; Int32 Binding; Uint32 Pad;}[StorageOverrideCount] (the storage-block override map is name-keyed BY DESIGN, ProgramObject.h:1030-1034; the name travels as a host span staged whole). Signature = the client's commutative hash of the override map, the same function as Managers.cpp:10832-10841, so the server's rebuild clause is one Uint64 compare. Bounds: BlockBindingCount <= 64, SamplerUnitCount <= 256, StorageOverrideCount <= 64, else Fatal{ProtocolCorruption, "SetProgramBindings.<field>"} — a larger sampler array is a counted refusal, not a truncation. Emitted BEFORE create_shader_state at the same validate point (a rebuild inside the verb must already see the bindings). Dirty rule: bit NewShaderBindings (Tracker.h:364-371, 419-448) already mixes the three counters; the emitter latches on (Cso, backendStateVersion, blockBindingVersion). A re-issued create clears the record's tails (GlobalConstants' rule, PipeApply.cpp:2903-2909). all three counts 0 = "defaults" MGPipeApplySetProgramBindings, the program twin's sync and memo keys
MGPProgramDesc::LinkStatus (a Uint8 in the descriptor's existing pad, MGPipeTypes.h:585-597; size unchanged, PipeCatalogueTest pins it) 1 = the link the archive describes succeeded. create_shader_state is re-issued on the same handle at every link that changes GetLinkVersion (today's rule) and the record's Serial moves with it; a failed link re-issues with LinkStatus = 0 and empty blobs IFF the frontend reports the program unlinked afterwards (§9 ruling 9 pins which). A draw on a LinkStatus == 0 program DECLINES on the server, as the frontend raises INVALID_OPERATION. not linked the program twin (SyncCurrentProgram)
MGPProgramDesc::Spirv[6] / Reflection blobs are NON-EMPTY on a live wire The client encodes the link artefacts once per link with ProgramArtifactsCodec into SEG_STAGE; the applier decodes into RECORD-OWNED LinkArtifacts/SpirvArtifacts and the companion pointers (ProgramEmit.h:230-260) are deleted under a transport. Size 0 with no companion is the existing trip wire Fatal{ProtocolCorruption} (PipeApply.cpp:2858-2871). A SharedPtr pin of the client's artefacts was considered and REFUSED: rule B, and Link() replaces them in place (ProgramObject.cpp:348-356). — the program twin's rebuild
MGPImageView::Access 0 = GL_READ_ONLY, 1 = GL_WRITE_ONLY, 2 = GL_READ_WRITE — the encoding MGPipeEncodeImageAccess already writes (MG_Impl/Pipe/ImageEmit.h:163-176). The decode MGPipeDecodeImageAccess lives in MG_Pipe/MGPipeValueTypes.h beside an enum so both roles pin one table; the server comment at DirectGLES.cpp:2662-2669 is deleted as stale. Any other value is Fatal{ProtocolCorruption, "ImageView.Access"}. read-only SyncImageTextureBinding
kMGPipeSubsystemBufferBindings = 1ull << 13 (MG_Pipe/MGPipe.h:105-109, bits 13..62 reserved) Owns dirty bits 15/16/17 (NewConstBuffers/NewShaderBuffers/NewSoTargets, Tracker.h:97-99) and the SetShaderBuffers emitter. Caps-required under a transport like bits 7–12; the default push mask becomes 0x3fff. Bit 13 requires bit 7 (a range names a Buffer handle). TrackerTest.cpp:385-387 is rewritten, not deleted. bit clear = the frontend walk (monolith / A/B only) MGPipeSubsystemForDirty, SubsystemForEmitter
SetHashSuppressor slots (SetHashSuppressor.h:65-66) SetShaderBuffers becomes THREE slots (SetShaderBuffersUniform/ShaderStorage/AtomicCounter) — one per Class, so an emission of one class never cancels another's; SetProgramBindings gets one. SetStreamOutputTargets stays "P4b" (unemitted). — the emitters
IpcTable::RunAhead = MOBILEGL_IPC_RUN_AHEAD (default 1, range 0..1); IpcTable::PresentCredit = MOBILEGL_IPC_PRESENT_CREDIT (default 1, range 1..8) Parsed unconditionally like every IPC knob (ConfigLoader.cpp:356-403). PipeVerify forces RunAhead = 0 beside BatchWaits = 0 (:377). RunAhead=1 on a server without the caps bit logs once "run-ahead requested, server does not publish kCapRunAheadApply" and runs lockstep. The stats line gains credit-waits. RunAhead=0 is the A/B control: identical server code, only the client's wait differs RunAheadArmed, EmitPresent
BufferState bind-point generations (client-only, #if MOBILEGL_PIPE_PUSH) One Uint64 per BufferBindPointTargets entry beside NoteBufferChanged (BufferState.h:69-75), bumped by glBindBufferBase/Range (GL_Buffer.cpp:1501-1520, 1624-1650) and SetNamedTransformFeedbackBinding. Not a wire field; listed because it is the emit rule's input and the pull build's object must not resize (the licence TextureState::NoteImageUnitTouched took at P5d r3). — bits 15/16/17's shutters

§2 ra — the wait rule

2.1 The barriered predicate (one function, two callers)

MGPipeBarriered(op, payload, st) =
    MGPipeWaitClassFor(op) != kWaitNone                                  // static column, §2.2
 || (MGPipeCallClassFor(op) == kCtxVerb && st.IsTransformFeedbackActive) // (i) XFB stays lockstep
 || (op == DrawVbo && (payload.Flags & kDrawClientArrays))               // (ii) client arrays

st.IsTransformFeedbackActive is MGPContextValues' value (MGPipeTypes.h:1698): on the client it is ctx.IsTransformFeedbackActive() at emit, on the server the applied value — set_context_values precedes the verb on the ring, so the two agree. Escalation (ii) is a refusal under run-ahead (§5.1), so on a run-ahead server it never reaches the sink; it is listed so the predicate is total. No other runtime escalation exists; adding one is an integrator ruling and a row here.

2.2 The static column, row by row

class rows why
kWaitReply every kReplySlot row (PipeCalls.def:115-202, 206, 221): GetCaps, ResourceCreate/Respecify, MapPersistent, FenceStatus/Wait, QueryAvailable/Result/Timestamp, SetTextureParams, ResourceSubData (texture half, §2.5), ResourceReadback, GetTextureImage, ReadPixels the answer is not derivable (R-5)
kWaitPresent Present the credit, §2.4
kWaitApplied ApplierReset; GenerateMipmap (kCtxObject, waits today for the reason at ClientSession.cpp:900-914 — until tx2 lands its VerbMipRes arm, then kWaitNone); SetStorageBlockBinding (kCtxVerb, resolves through GetProgramObject(GlName), PipeApplier.cpp:805-808; trailing: pg resolves through MGPStorageBlockBinding::ShaderCso and flips it); BeginStreamOutput, EndStreamOutput, PauseStreamOutput, ResumeStreamOutput, BindStreamOutput (§5.7); CopyFramebufferToTexture (the CopyTex endpoint still syncs the frontend texture, DirectGLES.cpp:9714-9735; trailing, tx2) each still pulls a row or probes; the wait keeps P5C's semantics for it
kWaitNone DrawVbo, LaunchGrid, Clear, Blit, MemoryBarrier, Flush, SetSwapInterval, BindShaderImage, PatchParameter; every kCtxState / kCtxCso / kCtxObject row without a reply (incl. ObjectDeath, ResourceSubData buffer half); ResourceDestroy, UnmapPersistent, FenceCreate/Destroy, FenceWaitServer; QueryCreate/Begin/End/Destroy/Counter rule F holds for their sinks after §4/§5

The column is inert until the integration commit publishes the caps bit (§6.2), after vi/sb/pg/tx2/fb.

2.3 EmitAndWaitTails after PublishAndNotify (ClientSession.cpp:877)

if (ownsReplySlot)              -> WaitForAppliedBudget + DrainEventRing + ReadReply   (unchanged)
else if (RunAheadArmed())
    switch (class)  kWaitNone:    return seq;
                    kWaitApplied: WaitForAppliedBudget + DrainEventRing + ReleaseFillPins(); return seq;
                    kWaitPresent: return seq;   // the credit wait ran BEFORE encode, §2.4
else                            -> today's :915-959 shape, byte for byte (lockstep, Magma, verify)

ReleaseFillPins() clears the four O-class SharedPtr rows of gPipeInputs (PipeInputs.h:801-809) on the GL thread once the barriered apply returned — under run-ahead the next fill may be a frame away and a pinned VAO/program must not make the apply thread its last owner. VerbBarrier==0 keeps its lockstep meaning (nothing but reply rows waits); under run-ahead it removes the kWaitApplied waits too and is the run-ahead NEGATIVE CONTROL: expected red is Fatal{UnmigratedPipeInput, ...} at the first barriered row's stale pull.

2.4 Present pacing

EmitPresent (EmitTables.cpp:977-1053): BeforeReadOnlyVerb(); if (m_presentsSent >= PresentCredit) WaitForPresentAck(m_presentsSent + 1 - PresentCredit, 30 s) — ShutDown returns, TimedOut is Fatal{PresentCreditTimeout}; then DrainEventRing, encode, publish, PumpControlPlane. Credit 1 = the client publishes frame N+1 while the server applies and swaps frame N: one frame of overlap, at most one frame of added latency. Budget at ~850 draws + ~0.36 MB pmap/frame: SEG_CMD ~70 KB + set/CSO records per frame against 8 MiB, SEG_STAGE ~1 MB/frame against 32 MiB, SEG_REPLY unchanged. The credit, never the bytes, is the steady-state backpressure; WaitForCmdSpace and the stage bell (PipeWireCodec.cpp:836) are the safety net and both drain events on exit (§2.6).

2.5 Drains and forced waits

Drain points: every wait exit in ClientSession (reply, applied, present-ack, cmd-space, stage bell), the four EGL RPC returns (BackendObject_Remote.cpp:200-245), Stop. Forced waits: MapBuffer(READ) / GetBufferSubData / a CopyBufferSubData source → SyncGpuWrites (reply row, unchanged); glFinish → WaitForApplied(LastPublishedSeq) + drain (a new client Finish slot; Flush/Finish stay no-ops on the wire, ARCHITECTURE:415); every Server* EGL forwarder waits WaitForApplied(LastPublishedSeq) BEFORE the RPC (the mailbox is pumped between drain batches, ServerLoop.cpp:385-390, so a make-current would otherwise land between two run-ahead records); ServerSwapEGLBuffers is not on that list — present is the swap. glGetError relaxation: a run-ahead verb's kEventGlError is observed at the next drain point, i.e. at most one present credit later than the call after it; P5C §4.2 already assigns ordering to P9 and P5e names the window. resource_subdata: the BUFFER half passes wantReply = false through MGPipeRouteResourceSubData (the Bool is discarded today, PipeFill.cpp:839; the server still posts, the slot is never read — legal, ReplySlot.h:16, 105), which is what makes the persistent-map push (PersistentMapTracker.cpp:735, 850) fire-and-forget; the TEXTURE half keeps its wait because DrainTextureSubData clears the level's dirty flag on the accepted reply (D-D5; trailing).

2.6 Event-ring flow control (ARCHITECTURE §11.7 made real)

On a run-ahead server Reserve == nullptr is NOT Fatal{EventRingOverflow}: the producer latches eventRingFull (exists, EventRing.h:135-143), publishes, rings, and ApplyThreadMain parks at the record boundary with eventRingFull == 0 added to ready (ServerLoop.cpp:380-383). The client re-checks m_events.RingIsFull() at every park exit and drains before re-parking. Deadlock argument: the server blocks only on the event ring; the client blocks only on watermarks the server advances; every client wait drains; so at most one side is parked at any instant. The lockstep arm keeps the Fatal (P5C §4.4).

2.7 Object lifetime under run-ahead

The client frees a slot only after its death record went out (PipeFill.cpp:1623-1626), the ring is in order, every record naming {s,g} precedes destroy{s,g} which precedes create{s,g+1}, and every server twin table Gen-checks (§4.3). No wait is added at any delete. Persistent maps: Forget() (PersistentMapTracker.cpp:599-606) drops tracker state only; pushed bytes were copied into SEG_STAGE at StageOptional and reclaim waits on retiredSeq, so a map released with blocks in flight is safe. The P5d deferred-destroy queue (PipeMutation.h:216-236) STAYS LIVE: a barriered record's apply may still pin a frontend object (its fill's SharedPtr rows, XFB targets), so the apply thread can still be a last owner. Under RunAheadArmed an enqueue from an UNBARRIERED apply is a finding: MGLOG_E_ONCE with the kind, Fatal under strict.


§3 gPipeInputs — server-role memory for unbarriered records

  1. The client fills only for barriered records. In MGPipeValidateForVerb (PipeFill.cpp:2962-3306), when RunAheadArmed() and !MGPipeBarriered(...): step 2 (tracker walk) and step 3 (emitters) run unchanged; :2989 (++CurrentVerbSerial), :2999-3001 (SetVerb/SetIdentity), step 4 (:3252-3270, CopyField + FilledGen) and MGPipeLeaveVerb's writes are SKIPPED; MGPipeNoteFrontendMutation returns at :554. The residual-value trip wire (:3284-3294) still emits — it compares on the server against the applier mirror. RunAhead=0 on a caps-publishing server uses the SAME fill decision (the A/B changes one variable: the wait). PipeVerify forces lockstep, a fill for every verb, and every record barriered (both roles read the in-process flag; the comparator :3304 is inproc-only).
  2. The server's stamp is server-private. MGPipeServerStampVerbBoundary writes m_filled/m_currentVerb/m_serverStampedVerb into applier-owned storage, never into the shared block; MGPipeServerClearVerbBoundary() from the GL thread (:2997) is DELETED under a transport. For a barriered record the server reads the client's fill (the client is parked) and its own stamp; for an unbarriered one it does not touch the block. SetIdentity (:3001) moves to ApplyOne, once per session. This removes the E note's hazard (b.4).
  3. The detector. MGPipeInputUnfreshRead (PipeInputs.cpp:191-224) under an unbarriered record takes StrictBarrierPullFatal (:60-66) regardless of StrictErrors; the seven sticky forwards (MGPipeStickyForwardPull, :251-257) the same; CountBarrierPull stays for barriered records. This is what turns integration-split-strict into a hard green lane (§7).
  4. VertexAttribDefaultsOf (PipeFill.cpp:84-86) diffs against the emitter's own last header (g_attribDefaultLastHeader, :2949), never against the block.
  5. RefusePipeInputsTouchWhileApplierOwnsIt (ClientSession.cpp:1015-1056) is re-derived: the BatchWaits early return at :1037 goes; under RunAheadArmed any GL-thread write to the block outside a barriered fill is Fatal{RoleViolation, "gPipeInputs"} by name. Fatal{BarrierViolation} (:840) stays for BatchWaits==0 && !RunAhead.

§4 id — handle-keyed twins

4.1 The rule

With a live wire, every twin on the apply thread is resolved BY THE HANDLE the applier state or the record carries, never by a frontend object: GetOrCreate(MGPipeHandle) (SlotTables.h:294-342), FindByHandle (:420-426), ReleaseByHandle (:394-408). HandleOf(stateObj), Find(StateObject*), GetOrCreate(const StatePtr&), NoteStateForHandle/StateForHandle and Entry::stateRef survive as the MONOLITH-GLUE half (§5.8) and are unreachable from an unbarriered apply. The twin sync bodies take (MGPipeHandle, const <Record>&) and store m_handle (the m_pushedSyncHandle pattern, Managers.h:1857); the object parameter is deleted from the transport overload, not defaulted to null. The E note's "content-addressed VAO CSO" is corrected: every kind mints its slot off the object's own lifetime id (VertexInputEmit.h:170-172, TextureEmit.h:422, FramebufferEmit.h:257, ProgramEmit.h:411-422), so the rekey is a replacement of the lookup ARGUMENT, not of the table.

4.2 Per kind

kind / table key on the server the record that names the handle before any verb reads it twin created twin dies
Buffer g_backendBufferResources (already handle-keyed) Res from VertexBuffers[], IndexBuffer, BoundShaderBuffers[c][i], Desc.BufferForTexBuffer, VerbIndirect* resource_create/respecify EnsureBufferResourceForHandle(nullptr, h) resource_destroy
VAO g_backendVertexArrayObjects st.BoundVertexElements (PipeApply.h:594; one CSO per VAO, re-created on the same handle when the config moves) create/bind_vertex_elements first ResolveVaoTwin(handle) at a draw object_death{VertexElementsCso} / delete_vertex_elements
Texture g_backendTextureObjects BoundSamplerViews[u].Texture, BoundShaderImages[u].Res, FramebufferRecords[h].State.{Color[i],Depth,Stencil}.Res, VerbMipRes, VerbCopyTexDst, MGPCopyImage src/dst resource_create/respecify (Texture) first SyncTextureToBackendByHandle(h) object_death{Texture} / resource_destroy
Renderbuffer g_backendRenderbufferObjects MGPSurface.Res with Kind == Renderbuffer resource_create/respecify (Renderbuffer) first attach sync object_death{Renderbuffer}
Framebuffer g_backendFramebufferObjects st.BoundFramebuffer[t], record.Fbo, VerbBlitReadFbo/DrawFbo; kMGPipeDefaultFramebuffer is never a twin set_framebuffer_state GetOrCreateByHandle(record.Fbo) (already, DirectGLES.cpp:3197) object_death{Framebuffer}
Program g_backendProgramObjects st.DrawProgram / DispatchProgram / BoundShaderCso; composites at the band create_shader_state (+ set_program_bindings) first SyncCurrentProgram(handle) object_death{ShaderCso} / delete_shader_state
Sampler g_backendSamplerObjects the CSO family ONLY: st.BoundSamplerStates[u] or TextureResources[h].Params.BuiltinSampler create_sampler_state ResolveSamplerCsoTwin (already, Managers.cpp:13457-13486) object_death{SamplerCso}
SamplerView g_backendSamplerViews BoundSamplerViews[u].View (no draw-path resolver today; unchanged) create_sampler_view — object_death{SamplerViewCso}

The identity family of samplers (ResolveUnitSamplerBackend DirectGLES.cpp:4985-5026, the mint at :5623-5630, Managers.cpp:13264-13280) is deleted under a transport; a unit whose BoundSamplerStates[u] is null while the frontend held a sampler is a missing record and §5.3's window rule makes it unrepresentable. The creation moment is LAZY everywhere (no twin at create_*); the applier's Gen check refuses a bind or verb naming an unknown or stale handle (PipeApply.cpp:2266, 2549, 2625, 2683, 2896), so a twin is never minted for an object the ring has not introduced. applier_reset touches neither twins nor object records (PipeApply.h:800-832).

4.3 ABA

Gen moves only on reuse (SlotAllocator.cpp:410-419). At the server every record naming {s,g} is applied before object_death{s,g}, which is applied before create{s,g+1}. GetOrCreate({s,g+1}) on a live {s,g} entry resets it (SlotTables.h:338); FindByHandle({s,g}) after the recycle answers null (:424); a backward {s,g-1} is refused (:331). Even with a death skipped (an object that never crossed emits none, WireTables.cpp:595-596) the forward-Gen reset answers. Twin memos key on {slot,gen} plus server serials, never on a pointer. BackendSlotTable gains the composite band (m_band, indexed slot - base, as SlotAllocator.h:144 / PipeApply.h:511) so a pipeline program (slot >= 983040) does not grow the ordinary table to kMaxHandleSlot entries — a P5e prerequisite, not a follow-up, because the rekey makes it the ordinary path.

4.4 The scopes and the guard

MGPipeRefuseAllocatorFromApplyThread (SlotAllocator.cpp:22-38) exempts a probe iff a scope is active AND MGPipeApplierCurrentRecordIsBarriered() (server-private, set by ApplyOne from §2.1; true for every record on a non-run-ahead server, §6). MGPipeFrontendKeyedRegistryScope is deleted at its draw-path sites — DirectGLES.cpp:1609, 1734, 2588, 3303, 4236, 4329, 4585, 4926, 4993, 5328, 5689, 6729, 6775, 9695 and Managers.cpp:256, 2938 (draw-path callers), 4544, 5882, 7087, 8481, 8717, 9256, 9310, 9709, 12949, 13273, 13570 — with the frontend probes they wrap; the class survives only around the barriered-row sites (DirectGLES.cpp:6166, 8837, 9161, 9216, 10029, 11884, the verify arms Managers.cpp:10382, 10403) with their P8/P9 phases re-annotated. MGPipeReverseAnnouncementScope is renamed MagmaP7AllocatorDebtScope, compiled for the Magma sites only (VulkanRenderer.cpp:4554, 4634, 8986, ResourceTracker.h:723), exempting only when the server backend is DirectVulkan. The renderbuffer cross-check Managers.cpp:9709-9720 (not transport-gated today, unlike :9628-9632) is deleted by the rekey, not gated. The NoSession server-only fixture hop (Managers.cpp:224-246) is deleted; such a fixture's twins die at Stop.


§5 Per family — the clean gate, the records, the windows

5.1 VAO / vertex input / index (vi)

Twin from st.BoundVertexElements; memo key {BoundVertexElements, rec->ContentSerial, VertexBuffersSerial, IndexBufferSerial} (all server-owned); the buffer walk is over st.VertexBuffers[Start..+Count).Res (dedupe on {slot,gen}) with EnsureBufferResourceForHandle(nullptr, Res) and IsBufferDrawCleanByHandle(h, res, nullptr); ResolvedDrawBuffers::Entry::frontend leaves the split arm; the IBO arm reads st.IndexBuffer.Res

  • IndexBufferSerial (also for the restart substitution, DirectGLES.cpp:6447-6461); SyncCurrentVertexAttributeValues keys on {elementsHandle, ContentSerial, activeMask} and reads rec->Attributes[i].Enabled; the dead GetConfigVersion read at :4775 is deleted. Client-memory vertex arrays are NOT refused under split today (only CLIENT_INDICES/CLIENT_COMMANDS/ UNBOUND_PARAMETER, EmitTables.cpp:651-685; the server dereferences attrib.Offset as a raw pointer, Managers.cpp:5382): under RunAheadArmed a draw with kDrawClientArrays is Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"} raised on the GL thread in EmitDrawRecord's array arm; the lockstep arm is unchanged; the staged form (a per-attribute {BindingIndex, MGHostSpan} tail like kDrawHasUserIndices) is P8's. Two pins by test: CurrentBufferMutationEpoch() (Managers.cpp:794) is apply-thread-only under a transport; the VAO twin key is 1:1 only while the vertex-elements CSO stays identity-addressed.

5.2 Textures (tx2)

Every draw-path entry is SyncTextureToBackendByHandle(h, imageBindable): record first, twin by GetOrCreateByHandle, three syncs (h, *rec); the by-value twin copy + second Find (DirectGLES.cpp:1745-1755, 1781-1805) go with the map arm. Clean = m_isInitialized && m_syncedResourceSerial == rec->Serial && rec->PendingUploads.empty() && m_syncedParamsSerial == rec->ParamsSerial && !rec->Params.ForceResync && !m_forceTextureParamsResync && m_syncedBuiltinSampler == rec->Params.BuiltinSampler && m_syncedBuiltinSamplerSerial == SamplerCso(rec->Params.BuiltinSampler)->Serial && !rec->Params.SamplerResync && !m_forceSamplerResync && rec->Desc.StorageKind == Mipmap — no new state. The four in-body live reads (GetTarget at Managers.cpp:8876-8877, 8620-8621; IsTextureView :7060; the TexBuffer backing :8223-8231) read Desc.Target / Desc.ViewOf / Desc.BufferForTexBuffer; GetExternalIndex in logs becomes Desc.GlNameForDiag. The unit work list is {Res, backend} keyed (ContextSerial, SamplerViewsSerial, SamplerViewStart, SamplerViewCount, g_backendContextGeneration); PairingsIntact is deleted; ResolveAndBindUnitTextures is one pass over the window with the target from SamplerViewCsos[View].View.Target, then an unbind of every target the window did not claim driven by g_boundTexturesCache; the alias arbiter dies (the client arbitrated, SamplerEmit.h:751-757). Named delta (P5e-5): the unit list syncs the program-resolved texture per unit, not every slot of every touched unit; coverage is unchanged (views ∪ image units ∪ attachment lists ∪ the waited texture ops). RequireImageBindableStorage's re-dirty arm (Managers.cpp:5930-5962, already aborting three frames deep) becomes a NAMED refusal at its entry under a transport, Fatal{UnmigratedEmulation, "image-bindable-redirty"}; ImageBindableHint is the prevention, P9 owns the pull. GenerateMipmap(target) under a transport resolves the texture from VerbMipRes (as EnsureGenerateMipmapStorageAllocated already does, DirectGLES.cpp:8736), never the active unit (:9563-9566), and its row flips to kWaitNone. m_force*Resync are server-set and never cleared by the wire; the wire bits are never cleared by the server (D-E2, now load-bearing for a gate). The pinned guard list (MipmapStorage.cpp:39-51, RemoteClientTest.cpp:1938-1978) gains the six shape accessors it exempts today plus GetSamplerObject/GetTextureParamsVersion/GetContentVersion.

5.3 Samplers and the two unit windows (tx2; the rule binds the emitters)

The effective sampler of unit u is st.BoundSamplerStates[u] if non-null else TextureResources[view.Texture].Params.BuiltinSampler; its values are the CSO record's (MGPipeValueTypes.h:462-486); the raw-depth-fetch substitution stays on the server (ARCHITECTURE §5.5); texture2D->GetFormat() becomes SamplerViewCsos[View].View.InternalFormat. Window rule (A8 closed): set_sampler_views and bind_sampler_states MUST carry Start == 0 and Count >= MaxTouchedTextureUnit + 1 (0 when none touched), as SamplerEmit.h:733-737 emits today; at every draw/dispatch apply the sink checks the two counts against the applied MGPContextValues::MaxTouchedTextureUnit and a narrower window is Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count" — a server-side re-derivation is refused. The BindCurrentTextures memo key is {DrawProgram.{Slot,Gen}, ShaderCso.Serial, BindingsSerial, SamplerViewsSerial} (ruling 6). The decline arms DirectGLES.cpp:5152-5169 and :2073-2098 become named refusals under a live wire; CaptureUnitBindings/UnitBindingsUnchanged/g_observedUnitBindings (:1867-1957) are monolith-only text.

5.4 Framebuffers, attachments, images (fb)

SyncCurrentFBO is SyncCurrentFBOByRecord only; a decline under a transport is Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"}; FramebufferRecordMatchesBinding (:3017-3033) is monolith-only. BackendFramebufferObject::SyncToBackend(record, target): the attachment walk is over Color[0..7] + Depth + Stencil (Color[0] alone when IsDefault) — the record's 11 surfaces ARE the point set (the emitter refuses a point at or above the wire width, FramebufferEmit.h:489-501; FRONT/BACK tokens exist only on the default framebuffer); the empty point is Kind == None || Res null for the detach (Managers.cpp:10310) and the N-6 refusal (:9601-9606) alike; m_syncedFrontendAttachmentVersions is replaced by the record hash; g_attachmentBackendIdGeneration stays. SyncAttachmentSurface(target, surface, attachment) takes no object; storage through SyncMipmapsToBackendByHandle / RBO SyncToBackendByHandle(surface.Res). The two attachment texture lists key (record.Fbo, ContentHash, ContextSerial, g_backendContextGeneration) with entries {Res, backend} and PairingsIntact = entry.Res == surface.Res. ForceBindCurrentFBO is handle-only and stamps nothing frontend (the stamps at :4734-4738 and the legacy memo are skipped together under the transport test). The named blit takes two const MGPFramebufferState& (draw buffers, ReadSurface, Kind, layer/level/layered/ format from the surface, Samples from the record, kMGPipeDefaultFramebuffer for the default) and retires StateForHandle; the bound arm reads FramebufferRecordFor(st.BoundFramebuffer[t]). ScopedDetachedTextureFramebufferAttachments walks a reverse index texture → FBO slots maintained at set_framebuffer_state apply. MGPFramebufferState::Complete is never a "may I draw" gate. SyncReadFramebufferTextureAttachments is not gated on the framebuffer bit (:2227-2230), so its retirement moves the object A/B baseline in BOTH arms (re-baselined, not a regression). Default-framebuffer resize: the surface-changed consumer's AllocateStorage (ClientSession.cpp:210-221) reaches MGP_NOTE_AGGREGATE(FramebufferAttachment) (TextureObject.cpp:84-102), so bit NewFramebuffer (Tracker.h:594) re-emits the record — verified at this head, pinned by a test in fb. Images: SyncImageTextureBinding(const MGPImageView&): twin by FindByHandle(Res), Access decoded, target and storage format off the TWIN (no new wire field; MGPImageView is 24 B, no pad); ResolveShaderImageRecord's identity test and I5 seam log go. The sweep gate is (ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration) — never a backend re-mint counter, so the "re-minted inside the sweep" property (:2890-2901) holds; the walk stays the UNION of the record window and g_imageUnitHighWaterMark (:2811-2821), never narrowed to the window. MarkWritableImageBufferTexturesGpuWritten is DELETED under a transport (the client owns the GPU-write set, GpuWritePending.h:9-55, :83-115); WritableMask survives as the record of which points P9's narrowing will name.

5.5 Programs (pg)

Twin from the ShaderCso handle; SyncToBackend(h, const MGPipeShaderCsoRecord&) answers every reflection question from the record-owned archive and the bindings tails (GetLinkedShaderSnapshot :12049 is a debug log, deleted on this arm). The nine-clause clean condition (DirectGLES.cpp:4437-4507) keeps its clause count and moves its inputs: record.Serial vs m_syncedShaderCsoSerial (exists, stamped at Managers.cpp:12944-12966, unread today — P5e is its first reader), BindingsSerial, record.Signature, Desc.SpirvStatus, Desc.LinkStatus; ImageUnitFormatsStillMatch and the patch clauses are fb's / value rows. The stash g_currentDrawFrontendProgram (:4097-4098) becomes {MGPipeHandle, twin*} compared against st.DrawProgram; PrepareForCompute reads st.DispatchProgram. The sampler-pass memo (:5522) keys on BindingsSerial; sampledTargetForUnit reads the sampler-unit tail and the archive's uniform types. MapUBO() has no run-ahead answer: when ResolveGlobalConstantsRecord declines under a transport it is Fatal{UnmigratedVerb, "set_global_constants"}, not a torn upload. GetProgramObject/ ValidateProgramName keep their P9 rows: GetBackendProgramId(name) (:6151) is a monolith entry; ShaderStorageBlockBinding (:10020) is reached from the barriered set_storage_block_binding (§2.2) until pg's trailing item resolves it by MGPStorageBlockBinding::ShaderCso. Codec completeness: ProgramArtifactsCodec.cpp:242's skipped member is named and either encoded or proven unread by the twin before pg claims "every reflection question is answered".

5.6 Buffer binding points (sb)

Applier: BoundShaderBuffers[3][84], Start[3]/Count[3]/WritableMask[3], ONE ShaderBuffersSerial advanced on every applied record and ADVANCED (never zeroed) by MGPipeApplierReset (VertexBuffersSerial's rule, PipeApply.h:609-616); the emitter's latch resets with it (PipeFill.cpp:3129-3143's list). SyncBufferBindingPointsByRecord(Class, glTarget): per entry EnsureBufferResourceForHandle(nullptr, Res); whole-vs-range is Offset == 0 && Size == kMGPipeWholeBuffer; clean = resource->syncedChangeSerial vs the resource record's Serial (Managers.cpp:3151) plus ShaderBuffersSerial; no twin key is introduced (a point names an object, it is not one). The UBO loop (DirectGLES.cpp:5447-5497) reads BoundShaderBuffers[Uniform][binding] where binding is the program's block binding (pg's tail); binding >= Count = nothing bound; GetBufferResource(obj) :5472 becomes FindBufferResourceForHandle. SyncAtomicCounterBuffers reads Count[AtomicCounter]; GetBufferBindingPointCount's sticky forward becomes FATAL under split (InvalidateCompileEnv's shape, FieldOwnership.def:163-165). The three backend GPU-write marks (DirectGLES.cpp:606, 658, 2775) are deleted under a transport in ONE edit with fb's. Client shutter (the P4b hole, verified: bits 15/16/17 read the content aggregate, Tracker.h:525, 636-639, and glBindBufferBase moves only BindingSlot::m_version): bit 15 = Mix(bindPointGen[Uniform], programIdentity), bit 16 = Mix(gen[ShaderStorage], gen[AtomicCounter]), bit 17 = Mix(gen[TransformFeedback], TransformFeedbackGeneration); the content aggregate leaves all three. Touch joins gen_pipe_dirty_surface.py's MUTATOR_PREFIXES (:87-90) and DirtySurface.def gains the rows the generator then demands. Coverage.def's EMITTED list gains GetBufferBindingPoint → SetShaderBuffers as SHAPE-ONLY; FieldOwnership.def:80-81 flips to RECORD_SUPPLIED when the four Espryt consumers are gone. GetBufferBindingSlot's 15-pointer copy (PipeFill.cpp:116-124) needs no special case: an unbarriered record has no fill at all.

5.7 XFB — stays lockstep

StartPendingTransformFeedback (:1381-1510), SyncTransformFeedbackBindingPoints (:562-592), ReadbackCapturedRanges (:1118-1160) and the xfb.targets pins are not migrated (GetTransformFeedbackProgram is an object row no package retires; the targets hold SharedPtrs across two verbs, which rule C forbids an applier entry to do without a barrier). Escalation (i) makes every verb inside an active span barriered; the cost on the Espryt steady path is one bool test per draw (:1386).

5.8 Monolith / G1 / the push build under Transport=monolith (ruling 1)

Every server-side handle arm is selected by MG_Config::Transport != Monolith (the idiom at DirectGLES.cpp:3226, 4725, 6534, Managers.cpp:9629) AND the family bit; it runs under ANY active transport, lockstep or run-ahead, so RUN_AHEAD=0 is a pure wait-rule A/B on identical server code. The push build under Transport=monolith keeps its frontend arms token for token (behaviour and bytes), which the verify comparator needs; the pull build compiles none of it (every new symbol sits under MOBILEGL_PIPE_PUSH / MOBILEGL_BUILD_DISAGGREGATED; the TwinRegistry alias keeps its mangled name, Managers.h:626-640; BackendTextureObject's pull-build layout does not move — the admitted-resize set stays empty). Two overloads, not an #if inside one body — the frontend one is visibly the monolith-glue half, as GetOrCreate(const StatePtr&) is (SlotTables.h:236-244). G1 0/0/0/0, G2/G14 name-set rules kept; the new public names are listed in BRIEF-P5E per package. MOBILEGL_PIPE_VERIFY forces lockstep (ConfigLoader.cpp:377, confirmed).


§6 Magma keeps the lockstep

  1. The DirectVulkan arm of CallMask never sets kCapRunAheadApply; RunAheadArmed() is false; the client runs today's :915-959 path; MGPipeApplierCurrentRecordIsBarriered() answers true for every record on a server that does not publish the bit, so Magma's probes inside MagmaP7AllocatorDebtScope and its BARRIER_PULLED reads keep P5C's semantics and rsp accounting. P7 retires them.
  2. On Espryt the bit is published by Init.cpp's DirectGLES arm gated on one constant (kMGPipeP5eRunAheadReady, false until the integration commit), so every package lands with the column inert and nothing changes behaviour until the integrator flips it.
  3. Negative controls: MOBILEGL_IPC_RUN_AHEAD=1 on Magma logs once and runs lockstep; MagmaPipeIdentityTest/ServerLoopTest pin that a DirectVulkan CallMask has no bit 10.

§7 The strict gate

integration-split-strict (.github/workflows/test.yml:1140-1206) becomes a HARD GREEN lane for the Espryt run-ahead server: with MOBILEGL_IPC_STRICT_ERRORS=1 and the caps bit published, every scenario must complete and every [BARRIER-PULLED, ...] marker in a private log is red. Because §3.3 aborts on an unbarriered pull regardless of the knob, strict adds only the barriered rows' pulls (readbacks, GenerateMipmap until tx2, XFB, CopyTex, set_storage_block_binding) — the lane's step lists those rows as the ONLY admitted markers (an allowlist by <field>@<verb>) and fails on any other. A revert of any handle arm in vi/sb/pg/tx2/fb goes red HERE, by field and verb, not in a picture. Magma keeps the expected-red step. rsp on unbarriered records is 0 in every scenario summary (pinned: a non-zero count would be a Fatal that did not fire). The PipeSlotPeek harness asserts zero scope entries per frame on the draw path.


§8 Amendments to CONTRACT-P5C (the complete list)

  1. §3.1's two named exemptions are superseded by §4.4: the registry scope is deleted at every draw-path site and exempts only a barriered record elsewhere; the announcement scope is Magma's.
  2. §4.4 overflow: lossless stays, but a run-ahead server parks on Reserve == nullptr (§2.6).
  3. §5.2 object_death: the ring keeps the ordering; the row is kWaitNone.
  4. §5.1 applier_reset is kWaitApplied; its ContextSerial assertion stands.
  5. §6 CI lane: the two-sided lane becomes one-sided green for Espryt (§7).
  6. §6 layer 2 gains Fatal{RoleViolation, "gPipeInputs"} (§3.5); InBarrierWait() is re-derived as MGPipeBarriered. 7. §3.3's state note is retired by fb (§5.4).
  7. FieldOwnership.def: GetBoundVertexArray, GetTextureUnitObject, GetImageTextureBinding, GetFramebufferBindingSlot, GetProgramForDraw/Dispatch, GetBufferBindingPoint gain retiring phase "P5e (Espryt unbarriered), P7 (Magma)"; GetBufferBindingPointCount's forward → FATAL under split; GetTransformFeedbackProgram, GetProgramObject, GetTextureObject, ValidateProgramName, HasOpenTransformFeedbackSpan, RecordError keep their rows (barriered-only readers).
  8. Table 1 gains set_program_bindings (80), appended; set_shader_buffers (38) gains its route, sink and emitter (PipeCatalogueTest.cpp:170-171, 242-243 invert). Table 0 gains §1's thirteen rows. Table 3 gains IpcTable::RunAhead/PresentCredit.
  9. P5C §6's rsp paragraph: the measured object-class pulls (948.5/frame on bsl) go to 0 on unbarriered records; what remains is the barriered allowlist of §7.

§9 Rulings the integrator makes at c0e (recommendation first; BRIEF-P5E §6 has the conflicts)

  1. Monolith arm selection: transport check + family bit, frontend arms kept for monolith (§5.8) — NOT S7's "handle arm under monolith too" (it would make the monolith push build depend on the new emissions and lose the verify comparator's frontend arm).
  2. Client vertex arrays: refuse under run-ahead (§5.1), stage in P8; wait-escalation rejected (it would need Find(vao) on the apply thread, which §4.4 forbids for a draw).
  3. XFB / atomic dispatch waits: the wire-derived predicate (§2.1); atomic-counter dispatch needs no escalation because sb lands 4.5.
  4. Present credit default 1, range 1..8; credit 2 is a measurement arm only.
  5. resource_subdata: buffer half kWaitNone, texture half keeps the reply (trailing D-D5).
  6. BindCurrentTextures memo key: both S2's and S4's rows (§5.3).
  7. Image sweep gate: applier serials, never a backend counter (§5.4); keep the union walk.
  8. Program archive: encode per link into SEG_STAGE (§1), measured on MC + a shader pack before pg is sized; the SharedPtr pin is refused; lazy encode at first bind is the fallback.
  9. Failed relink: re-issue with LinkStatus = 0 iff the frontend reports the program unlinked afterwards; pg verifies which at ProgramObject.cpp:348-356, 518-522. Never an object_death.
  10. set_shader_buffers window: the touched high-water count per target; capacity 84 on the wire, the backend clamps (DirectGLES.cpp:500-504). 11. Suppressor: three class slots.
  11. Magma scope: one renamed scope, backend-kind-keyed exemption (§4.4).
  12. Deferred-destroy queue: stays live (§2.7). 14. ResolvedDrawBuffers: measured after vi.
  13. Blit's kWaitNone is fb's landing, not a separate package; Clear's is tx2+fb.