56 KiB
CONTRACT-P5 — the four tables every P5 package is held to
Authority: this file. ~/w7/notes/p5/BRIEF-P5.md states the rulings R-1…R-14; this file is
where they become implementable, and where the rulings the brief left to the contract package
are made. Where the two disagree, this file is newer and this file wins — and §6 lists every
place they disagree, so nobody has to diff them.
How to change it. MG_Remote/CONTRACT-P5.md is c0's file. A package that needs a row
changed goes through the integrator, who edits here first and only then lets the package land.
P4a's contract was corrected seven times and each correction cost a package a rework round;
the point of this file existing at all is that a correction is a one-line diff here rather than
a rediscovery in six places.
It lives beside the code, not in ~/w7/notes/, because it describes bytes on a wire and has to
move with the branch that defines them.
Base: feat/disaggregated @ a29807cc. Every file:line below was read at that commit.
§0 The three rules that sit above every table
These are R-2 and R-11 in their formal wording. They apply to every row of table 1 without restatement, and every reviewer's first three questions (BRIEF §11.1, §11.4) are these three.
Rule A — a content record must declare its bytes. Under
MG_Config::Transport != Monolith, a record whose payload owns an MGPBlobRef and which
carries content must set Seg to a real segment, Offset to a byte offset within that
segment, and Size to a non-zero byte count. Blob.Size == 0 on such a record is
Fatal{ProtocolCorruption}.
This inverts today's legal state, and deliberately. MGPipeTypes.h:398-410 says in so many
words that a zero Blob.Size means "this record does not declare its blob" and "is not a
fault" — which is right for monolith, where the bytes travel beside the record as a companion
pointer. Under split there is no beside.
Rule B — no host pointer crosses. Under split, MGHostSpan::Ptr is written nullptr by
the encoder and any non-null Ptr seen by the apply side is Fatal{ProtocolCorruption}.
Today Ptr is the fast path (MGPipeHostSpan.h:51), which is exactly why this needs saying.
P5's reduced path should produce zero host spans at all — see table 0's cap-bit row.
Rule C — an applier entry point may not hold a pointer past its return. A SEG_STAGE run
is valid from publish until retiredSeq passes the record that named it. The tree has exactly
one violation and it is named rather than tolerated: Espryt's GLESBufferResource::hostBytes
(Managers.h:839), written by Ops_H_SubData (Managers.cpp:1980-1983) and Ops_H_FlushRange
(:2035), read by six later drains (:2000, :2062, :2080, :2111, :2741, :2843).
Under split those two writes must copy into server-owned storage. MOBILEGL_IPC_AUDIT=1's
0xDD fill over retired staging bytes (R-2.5) is the mechanical control that says whether they
did; without it, an inproc implementation that kept the pointer is indistinguishable from one
that copied.
§1 Table 0 — the encoding table
One row per wire field that is not a handle. Handles are {slot, gen} and are settled by
P2/P3a; everything else that is not a plain scalar is here.
| field | the ruling | zero means | who reads it | evidence / note |
|---|---|---|---|---|
| segment id space | SEG_CMD=1, SEG_STAGE=2, SEG_REPLY=3, SEG_EVENT=4, SEG_SHADOW=5, SEG_ADOPT=6. Identical values to Protocol::SegmentKind. |
"no segment", always. 0 is never a real segment id. | encoder, decoder, gMGPipeSegmentResolver |
protocol.fbs:36-44; kMGHostSpanSegNone = 0 at MGPipeHostSpan.h:21. The two are tied by static_assert in Wire/PipeWireCodec.cpp, which is the only place the flatbuffers header and the codec enum meet. kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF (:26) stays reserved for P8. |
MGPBlobRef{Offset, Size, Seg, Pad0} (24 B, MGPipeTypes.h:55-61) |
Seg from the row above. Offset is a byte offset inside that segment, never a host address. Size != 0 ⇔ "this record declares its blob", and under split a content record must declare it (rule A). |
Size == 0 = "no blob declared" — legal in monolith, Fatal under split for a content record. |
decoder, every bounds cross-check | Today every emitter writes {Seg=None, Offset=<host address>, Size=0 or real}. Five of the eleven blob members do declare a real size today (CsoCache.h:156, VertexInputEmit.h:398, ResourceTracker.h:216, PipeFill.cpp:2259); the rest write 0. See table 1's "length" column for which. |
MGHostSpan{Ptr, Seg, Pad0, Size, Offset} (32 B, MGPipeHostSpan.h:28-37) |
Split: Ptr == nullptr always (rule B). The 32-byte layout must not be reordered (:29-31). |
Seg == 0 with Size != 0 is Fatal. |
decoder | P5's reduced path must produce none at all. See the cap-bit row below. |
kCapNeedsHostIndexBytes (1<<7), kCapNeedsHostUboBytes (1<<8) |
Both are 0 for the whole of P5, by ruling. | — | client emitters | MGPipeTypes.h:120-123. This is the cheapest way to keep every MGHostSpan out of the first IPC frame: the two bits are the only things that ask for one. TriangleScenario must therefore use a VBO-backed draw and not client-array indices, or kDrawHasUserIndices (MGPipeTypes.h:1212) produces a span the split filling for which is P8's. |
MGPCaps (4 members, MGPipeTypes.h:126-139) |
One carrier, not two. MGPCaps is the model; protocol.fbs's CapsSnapshot is its transport form. Dynamic and CallMask cross as POD bytes; FormatCapabilities and RendererInfo cross as the two blobs whose serializers are P5's new work (MG_Remote/CapsCodec.h). |
— | CapsMirror (client), ServerSession (server) |
MGPCaps has only a compositional size assertion (MGPipeTypes.h:145-146) because DynamicBackendParameters still carries SizeT and GLenum; P0.5's fixed-width rewrite never happened. P5 does not rewrite it — see the ABI row. |
CapsSnapshot redundancy |
tableSlotMask (protocol.fbs:94) is DELETED, not renamed. maxComputeWorkGroupCount / maxComputeWorkGroupSize (:92-93) and prefersCpuXfbPrimitiveAccounting (:95) are deleted too: the first two ride inside Dynamic already (BackendObject.h:392-393), and the third is answered by kCapCpuXfbPrimitiveAccounting. |
— | s1 (the schema), c1 (the mirror) | R-8 offered rename-or-delete for tableSlotMask; delete, for two reasons and the second is decisive. ARCHITECTURE.md:114 says CallMask replaces "is this table slot null" as the capability probe, so a field whose comment is "which GLFunctionsTable slots the peer registered" re-introduces precisely what it replaced. And GLFunctionsTable has 69 function-pointer slots (BackendObject.h:117-292), so a ulong mask cannot address it and never could — it is five bits short on day one. |
MGPCaps::CallMask layout |
bits 0..8 = MGPCapBit, unchanged. bits 9..31 reserved. bits 32..47 = the consumer mask: bit 32+n means "the server has a consumer for MGPipe subsystem bit n". bits 48..63 reserved. |
a clear consumer bit = "this server does not consume that family; emit nothing for it". | CapsMirror::ServerConsumes — the only legal client-side source |
c0's ruling, and the thing that makes R-8 implementable at all: R-8 says the client's liveness gates must read the CallMask mirror, but CallMask as declared has only nine feature bits and no per-family bit. Constants and the two fold/test helpers are in MG_Remote/CapsCodec.h; CapsCodec.cpp asserts the block does not collide with MGPCapBit and that P4a's 0x1fff fits sixteen bits. |
| ABI agreement | Hello/Welcome assert both peers agree on sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable) and buildFingerprint. A mismatch is Fatal{AbiMismatch} and never a downgrade. |
— | s1 | The compositional assertion above means the caps block's literal size is ABI-dependent. P6's spawn is same-machine, same-binary and inherits this unchanged. A fixed-width rewrite of DynamicBackendParameters is P7's account, not P5's. |
MGPSubData::Target |
Packed: low byte = MGPipeResourceTarget, high byte = the cube-face upload target. Read only through MGPipeSubDataResourceTargetOf / MGPipeSubDataUploadTargetOf. Whole field == 0 is the buffer half; a low byte naming Buffer, Renderbuffer or >= MGPipeResourceTarget::Count is Fatal{ProtocolCorruption}. |
whole field 0 = buffer upload | applier | P4a ID-12. Already settled; copied here because a decoder that open-codes it is the class-1 defect. |
MGPImageView::Access |
The three-value encoding at ImageEmit.h:146-159, verbatim. |
— | decoder, applier | This encoding has only ever lived in a package header (P4a R-3). This table is its first wire reader, so it is now contract. |
MGPSamplerView::Target |
The encoding at SamplerEmit.h:900, verbatim. |
— | decoder, applier | Same status as the row above. |
MGPFramebufferState::DrawBuffers[8] |
The -1 / default-token narrowing at FramebufferEmit.h:146-163, verbatim. A draw-buffer entry naming a colour attachment outside the record's own array is Fatal{ProtocolCorruption} (the applier already says so). |
-1 = "no attachment" |
decoder, applier | Same status. |
MGPReplySlot::Id |
= the record's sequence number (R-3). No new id space, no allocator. The server writes the answer into SEG_REPLY[seq % slots] and stamps seq back into the slot header so a wrong-slot read is detectable rather than plausible. |
seq is 1-based; 0 = "no record / not encoded" |
client barrier wait | ARCHITECTURE.md:124: the wire carries no per-record seq field, so seq is the ordinal. MGPReplySlot exists (MGPipeTypes.h:78-81) and no payload of the ten kReplySlot calls contains one — which is exactly why the id must be derived rather than carried. P9 generalises this to "seq is the id's initial value", which extends the rule rather than overturning it. |
| reply slot header | {Uint64 Seq; Int32 Status; Uint32 Size;} — 16 bytes, then the payload. Status: 0 = OK, 1 = DECLINED, 2 = ERROR. |
— | client | DECLINED is a real answer, not a failure. It is how MapPersistent says nullptr (R-6) and how the four Bool acceptance entry points say false (R-5). A client that treats DECLINED as an error re-creates ID-39's 66 lost uploads from the other side. |
kRecPad and seq |
A wrap filler does not advance seq, on either side. | — | both | R-9. RingConsumer::Pop already skips fillers; the rule is stated because the counter is the caller's, not Pop's. A side that counts pads drifts by one per wrap, for ever — and since seq is the reply-slot id, a drifted seq reads another call's answer instead of failing. Pinned by RingTest.AWrapFillerDoesNotAdvanceTheRecordSequence. |
| per-opcode flags | kMGPipeCallFlags[MGPWireOp::kOpCount] in generated/PipeWire.inc, read only through MGPipeCallFlagsFor(op). Index 0 (kInvalid) is kNone. |
kNone = no flags |
every package | R-13.4. Before this table existed nothing generated exported the flags, so six packages were each about to hard-code PipeCalls.def's fourth column — which is how GetCaps and CreateSamplerState came to own an MGPBlobRef with no kHasBlob on their line. gen_pipe.py now also refuses a flag token that is not an MGPipeCallFlags enumerator, with two negative controls in --self-test. |
kHasBlob's meaning |
Exactly "the payload owns an MGPBlobRef member" — nothing weaker. |
— | decoder | PipeApply.h:78-79 already says so. Three calls carry bytes with no MGPBlobRef; they are table 1 rows 19–21 and are deliberately unflagged, because a decoder that trusts kHasBlob has to find a member to read. |
§2 Table 1 — the byte carriers
23 rows, not 19. BRIEF §3 lists 19 and scout-premortem:§3 lists a different 19; the
union is 23 and the four that only the premortem lists — MapPersistent, ResourceReadback,
ReadPixels, GetTextureImage — are precisely the ones whose bytes travel server → client.
Leaving them out of the byte-carrier table is how a phase discovers in week three that it never
decided where readback pixels land. They are rows 20–23 and are marked with their owning phase.
Columns: flags · blob member · companion pointer today · which segment the bytes
live in · who owns that memory · when the slot retires · who declares the length,
who cross-checks it · reply name (for kReplySlot rows).
apply = retires when DecodeAndApply returns. submit = when the server has handed the bytes
to the driver. gpu = completedFrameSerial.
Group A — kHasBlob, the payload owns an MGPBlobRef
| # | call (op) | flags | blob member | companion today | segment | owner | retires | length declared / cross-checked |
|---|---|---|---|---|---|---|---|---|
| 1 | CreateRenderState (17) |
kHasBlob |
Blob (MGPipeTypes.h:364) |
const void* chunkBytes (PipeApply.h:740), passed CsoCache.h:157 |
SEG_STAGE |
client stages, server copies on apply | apply | declared real (CsoCache.h:156, = kMGPipePipelineChunkBytes); nothing reads it today — MGPipeApplyCreateRenderState (PipeApply.cpp:1280) never touches Blob.Size. Decoder must cross-check against ChunkMask. |
| 2 | CreateVertexElements (20) |
kHasBlob |
Blob (:419) |
const void* blobBytes (PipeApply.h:932), passed VertexInputEmit.h:399 |
SEG_STAGE |
client stages, server copies | apply | declared real (VertexInputEmit.h:398); cross-checked, and this is the model for every other row: PipeApply.cpp:1990-1999 recomputes AttributeCount*sizeof(MGPVertexAttribWire) + BindingPointCount*sizeof(MGPVertexBindingPointWire) and refuses a disagreement; both counts bounded by kMGPipeMaxVertexAttribs. |
| 3 | CreateShaderState (27) |
kHasBlob |
seven: Spirv[6] (:507) + Reflection (:508) |
two typed frontend pointers — const LinkArtifacts* + const SpirvArtifacts* (PipeApply.h:1039-1041), passed ProgramEmit.h:257 |
SEG_STAGE, seven independent runs |
client stages, server copies | apply | all seven declare Size = 0 today and Reflection.Offset is literally (Uint64)&link (ProgramEmit.h:254). The serializer already exists and no package may write a second one: EncodeProgramArtifacts/DecodeProgramArtifacts (ProgramArtifactsCodec.{h:53,60,cpp:252,264}), its own suite, and the verify build already round-trips every real program it links (PinProgramArchiveRoundTrip, PipeApply.cpp:1056-1087, called :2638). |
| 4 | SetDynamicState (30) |
kHasBlob |
Blob (:389) |
const void* chunkBytes (PipeApply.h:748), passed PipeFill.cpp:2260 |
SEG_STAGE |
client stages, server copies | apply | declared real (PipeFill.cpp:2259); nothing reads it (PipeApply.cpp:1363-1370 scatters by ChunkMask). Same fix as row 1. |
| 5 | SetGlobalConstants (40) |
kHasBlob |
Blob (:848) |
const void* bytes = MapUBO()'s image (PipeApply.h:1052), passed ProgramEmit.h:195 |
SEG_STAGE |
client stages, server copies | apply | declares 0 (ProgramEmit.h:194); cross-check exists at PipeApply.cpp:2784 against Desc.GlobalUboSize but is inert while Size is 0. Under rule A it becomes live. Per program per frame, unbounded length — the row most worth watching against R-10's max-record counter. |
| 6 | SetResidualValueState (46) |
kHasBlob |
Blob (:932) |
none, and no record either: the entry point is MGPipeApplySetResidualValueState(const ResidualValueBlock&) (PipeApply.h:760), passed PipeFill.cpp:2184. MGPResidualValueState is never instantiated on the live path. |
SEG_STAGE |
client | apply | nothing declares it. The encoder must invent both the record fill and the blob fill. sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE == 8 is statically asserted (MGPipeTypes.h:924-927) and only ever ratchets down. This is the hardest row in the table and neither scout flagged it; see §6. |
| 7 | ResourceSubData (48) |
kHasBlob|kVarTail |
Blob (:1009) |
const void* bytes + const MGPSubRegion* regions (PipeApply.h:897-899); buffer half PipeFill.cpp:705, texture half TextureEmit.h:1278-1280 |
SEG_STAGE |
client stages, server must copy (rule C names this call) | apply | the two halves disagree today: buffer declares real (ResourceTracker.h:216), cross-checked at PipeApply.cpp:702; texture declares 0 (TextureEmit.h:1265-1267) on the grounds that the byte count is "the server's to compute once it has picked box-or-rects" — which cannot be a bounds check. Under rule A the texture half must declare too. Tail: MGPSubRegion[RegionCount] (:1007). |
| 8 | BufferSubDataResident (49) |
kHasBlob|kOptional |
Blob (:1009, same payload) |
const void* bytes = application staging, "valid for the duration of the call only" (PipeApply.h:90, :901); one caller, PipeFill.cpp:727 |
SEG_STAGE |
client stages, server copies | apply | declared real via MGPipeBuildSubDataRecord, cross-checked at PipeApply.cpp:702. kOptional is a capability question under split, not a null-pointer question: the client must gate on kCapResidentSubData through the caps mirror, never on a table slot (R-8). |
Group B — kVarTail, a tail and no blob member
Every row here declares its tail by a count, and MGP_WIRE_CHECK_BOUNDS cannot see the
tail at all — it only proves size >= sizeof(MGPWireRec_X), so a record declaring
Count = 4000 while carrying 8 bytes passes today. The decoder must recompute the total from
the declared count(s) and require it to equal MGPWireRecHeader::Size.
| # | call (op) | flags | tail element × count | companion today | segment | owner | retires | length |
|---|---|---|---|---|---|---|---|---|
| 9 | SetVertexBuffers (32) |
kVarTail |
MGPVertexBuffer × Count (:730) |
const MGPVertexBuffer* (PipeApply.h:941), VertexInputEmit.h:245 |
SEG_STAGE (tail follows the payload in SEG_CMD only if it fits the record bound) |
emitter-owned Vector, reused next emission |
apply | Count; no tail cross-check today |
| 10 | SetSamplerViews (35) |
kVarTail |
MGPBoundView × Count (:778) |
const MGPBoundView* (PipeApply.h:1028), SamplerEmit.h:786 |
as above | as above | apply | Count; Start+Count past the unit bound is already Fatal (PipeApply.h:1024-1026) — a slot bound, not a byte-length check |
| 11 | BindSamplerStates (36) |
kVarTail |
MGPipeHandle × Count (:785) |
const MGPipeHandle* (PipeApply.h:1029), SamplerEmit.h:860 |
as above | as above | apply | as above |
| 12 | SetShaderImages (37) |
kVarTail |
MGPImageView × Count (:802) |
const MGPImageView* (PipeApply.h:1030), ImageEmit.h:129 |
as above | as above | apply | as above |
| 13 | SetShaderBuffers (38) |
kVarTail|kHostSpan |
two tails: MGPBufferRange × Count (:826), then MGHostSpan × HostSpanCount (:828) |
none — no applier entry point exists. P5 writes the first producer and the first consumer. | SEG_STAGE |
— | apply | HostSpanCount is 0 or Count, never anything else (MGPipeTypes.h:820-823), so the two arrays stay index-aligned. kCapNeedsHostUboBytes is 0 for all of P5, so the second tail is always absent in this phase. |
| 14 | SetStreamOutputTargets (39) |
kVarTail |
two tails: MGPBufferRange × Count, then Uint32 × Count (:836-838) |
none — no applier entry point exists. | SEG_STAGE |
— | apply | one Count sizes both tails; off the reduced path in P5 |
| 15 | SetVertexAttribDefaults (41) |
kVarTail |
MGPAttribValue × Count (:864) |
const MGPAttribValue* (PipeApply.h:756), PipeFill.cpp:2115 |
as above | as above | apply | two declarants that must agree: Count and popcount(Mask) (:863, contract at PipeApply.h:754-755). A disagreement is a wire fault nothing checks today; the decoder must. |
| 16 | DrawVbo (59) |
kHostSpan|kVarTail |
MGPDrawRange × NumDraws (:1239), then a conditional MGHostSpan when Flags & kDrawHasUserIndices (:1212, :1231) |
none — no applier entry point exists. | SEG_STAGE |
— | apply | NumDraws; the span carries its own Size. The only kHostSpan on the hot path, and P5 must produce none of them — TriangleScenario uses a VBO-backed draw precisely so this tail never appears. MGPipeTypes.h:1221-1224 defers the fixed-head-versus-tail question to this phase: P5 keeps it in the tail, unchanged; there are no per-draw byte histograms yet to justify moving it, and moving it would be a wire-format change with no measurement behind it. |
Group C — carries content with no MGPBlobRef and no kHasBlob
These three are the reason kHasBlob had to be given an exact meaning (table 0).
| # | call (op) | flags | ruling | evidence |
|---|---|---|---|---|
| 17 | CreateSamplerState (23) |
now kHasBlob (R-13.1) |
MGPSamplerDesc does own an MGPBlobRef Parameters (:429) and the flag was simply missing. The blob is memcpy(sizeof(SamplerParameters)) — a POD, and borderColorForm must survive byte for byte (MGPipeTypes.h:425-428), because all three colour representations are always numerically populated and it is the only thing that says which one the backend must use. |
companion today is a typed frontend pointer, const SamplerParameters* (PipeApply.h:1000), passed SamplerEmit.h:458. Declares Size = 0 (SamplerEmit.h:433-435); cross-check at PipeApply.cpp:2353 is inert until rule A arms it. Padding trap: SamplerEmit.h:437-445 — assignment leaves three trailing padding bytes stale, and the bytes staged must be the bytes a later memcmp compares. |
| 18 | GetCaps (1) |
now kReplySlot|kHasBlob (R-13.1) |
MGPCaps owns two MGPBlobRefs, FormatCapabilities and RendererInfo (:137-138), and carried no kHasBlob at all. |
PipeCalls.def:80 before the fix. Serializers are P5's new work (MG_Remote/CapsCodec.h); the header itself defers them to this phase (MGPipeTypes.h:134-136). |
| 19 | ResourceRespecify (3) |
stays kNeedsAck, no kHasBlob (R-13.3) |
initialBytes is always nullptr under split. Initial content arrives as ResourceSubData records immediately after this one. MGPResourceDesc owns no MGPBlobRef and gains none. |
The alternative was costed and rejected: MGPBlobRef is 24 bytes, MGPResourceDesc's two pads are Uint16 Pad0 (:303) + Uint32 Pad1 (:315) = 6 bytes, so a blob member takes the struct 88 → 112 and moves MGP_ASSERT_POD(MGPResourceDesc, 88) (:320). The chosen route reuses a path that is already chunked (MGPipeForEachSubDataRecordRange, PipeFill.cpp:694-713) and already acceptance-gated; it costs one extra record. HasDefinedContent (:301) is the field the encoder branches on, and it already exists. The texture path already does exactly this — TextureEmit.h:1137 passes nullptr and relies on a following upload — so this generalises today's texture behaviour to buffers rather than inventing anything. |
| 19b | ResourceRespecify's second uncarried companion |
— | const MGPRespecifiedLevel* level (PipeApply.h:792-795, 4 bytes: Uint16 UploadTarget; Uint16 Level;) has no wire carrier either, and it is not bytes — R-13.3 does not cover it. Ruling: it rides in MGPResourceDesc's existing pads — Pad1 (4 B, :315) becomes {Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;} and one byte of Pad0 (:303) becomes Uint8 HasRespecifiedLevel. Zero size change, MGP_ASSERT_POD(..., 88) does not move, and PipeFields.def's MGP_FIELDS_MGPResourceDesc gains the two named members (pads are excluded from field lists, so this is required, not optional). |
Null means "this respecify redefines the whole resource" and drops every pending upload; non-null names the single (uploadTarget, level) and drops only that key. Clearing the whole set for a per-level glTexImage2D loses exactly the texels the server-side set exists to protect (PipeApply.h:805-820). Without a carrier, every OpenRA per-level respecify would silently take the whole-resource arm. c0 rules it and specifies it; c0 does NOT implement it — MGPipeTypes.h is unowned and the edit goes through the integrator, who must land it before w1 encodes this record. |
| 20 | ResourceFlushRange (51) |
stays kNone (R-13.2) |
It carries no bytes at all under split. It is a {range, AccessFlags} control record; the bytes of [Offset, Offset+Size) arrive ahead of it as ResourceSubData records covering exactly that range. |
R-13.2 offered "add a blobref" or "write the convention down". Neither, and for a reason: the ladder this record drives rewrites its range "from the authoritative shadow" (Managers.cpp:1047-1076), and under split the authoritative shadow is server-owned by rule C — so resource_subdata is already the only way bytes reach it, and a blobref here would be a second, forgeable way to say the same thing. AccessFlags must still cross verbatim, not normalised (PipeApply.h:902-903). Overturn condition: if the tier-1 INVALIDATE_RANGE arm turns out to need the bytes and the range in the same record — i.e. an intervening record could stale the subdata — this needs its own blobref. It cannot happen while the verb barrier holds, because nothing interleaves; revisit when the barrier retires for the buffer family. |
Group D — the four server → client rows the brief's list omitted
| # | call (op) | flags | ruling | reply name |
|---|---|---|---|---|
| 21 | MapPersistent (5) |
kReplySlot|kOptional |
Returns nullptr under split, always (R-6/R-2.4). Its const void* seedBytes companion (PipeApply.h:917) therefore never crosses in P5 and needs no carrier. The three frontend sites already tolerate a decline (BufferObject.cpp:238, :603-606, :657-660). Answer travels as Status = DECLINED with a zero-length payload. |
map_persistent.decline |
| 22 | ResourceReadback (52) |
kReplySlot |
Bytes go server → client in SEG_EVENT via OnBufferWriteback (#3), not in the reply slot: the destination is the client's shadow and the size is the resource's, not a fixed slot's. The reply slot carries only completion. The ordering rule is load-bearing: the writeback is applied before the mutation epoch bumps, never after (ARCHITECTURE.md:292-294, Managers.cpp:2120-2136). |
resource_readback.done |
| 23 | ReadPixels (58) / GetTextureImage (55) |
kReplySlot |
ReadPixels blocks in P5 and its pixels come back in the reply slot, which is why ReplyPool::SlotBytes() is sized from the scenario's largest read rather than guessed. MGPReadbackInfo has DstOffset/DstSize but no Seg (MGPipeTypes.h:1197-1206): ruling — the destination is always SEG_REPLY in P5, so no Seg field is added; the PBO destination (fire-and-forget plus a client-side MarkGpuWritten) is b1's and also needs none, because a PBO destination is a resource handle rather than a segment. GetTextureImage is not on P5's reduced path and its slot stays Fatal{UnmigratedVerb}. |
read_pixels.pixels |
§3 Table 2 — PipeInputs field ownership
This section is the SPEC and the four class definitions. The authoritative instance is
generated: package p1 writes the generator that emits generated/PipeFieldOwnership.inc plus
a --check, in the shape of gen_pipe_dirty_surface.py. A field in none of the four classes
is a build failure (R-7.1) — that is the whole mechanism, and a hand-maintained table would
be wrong within a week.
The domain is 63 fields (kMGPipeInputFieldCount, asserted generated/PipeFilled.inc:96)
plus the 7 sticky forwards, which are among those 63 but are exempted from the poison and
so need their own row. 70 rows, each in exactly one class.
The four classes
RECORD-SUPPLIED — a pushed record supplies the whole field, so the server never needs
the client for it.
Membership is kMGPipeFieldEmittedBy (generated/PipeFilled.inc:336-402,
kMGPipeEmittedFieldCount = 40) minus the nine for which
EmittedCallSuppliesTheWholeField returns false (MG_Impl/Pipe/PipeFill.cpp:1924-1939;
reasons :1875-1923). 31 fields today.
The nine excluded, with the generator's own reason: GetPixelStoreParameters ("only the PACK
half has a carrier"); GetCurrentVertexAttribute ("the applier cannot reproduce GLContext's
cross-view conversion"); GetMaxTouchedTextureUnit ("the set is hash-suppressed while the
high-water mark still moves"); and six sharing one reason — "the storage is a frontend heap
reference and the record carries an 8-byte {slot, gen}" — GetBoundVertexArray,
GetFramebufferBindingSlot, GetImageTextureBinding, GetTextureUnitObject,
GetProgramForDraw, GetProgramForDispatch.
APPLIER-DERIVED — the applier writes it from the records it already applies; no client
participation at all. Today: the render-state mirrors, m_pixelStore[0] (the pack half),
capability bits, the current vertex attribute, and the patch fields — written at
PipeApply.cpp:177-184, :1338-1437, plus everything MGPipeDeriveRenderStateFields
(PipeApply.h:1084, PipeApply.cpp:2157) derives.
BARRIER-PULLED — P5's debt, and every row names the phase that retires it. The server
answers by reading a value the client's residual fill put into the single shared gPipeInputs
while the verb barrier holds both threads apart. It is correct only because of that barrier,
which is why the barrier is load-bearing rather than cautious.
Each read increments PipeStats::CallClass::ResidualPulls (short name rsp, inside the
#if MOBILEGL_PIPE_PUSH block so G1 holds), published per frame. rsp's value at the end of
P5 is the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
MOBILEGL_IPC_STRICT_ERRORS=1 promotes every read in this class to Fatal, and a named test
asserts the abort actually happens — an instrumentation that cannot go red is decoration.
FATAL — no carrier, and the reduced path never reads it, so a read is a real defect.
Fatal{UnmigratedPipeInput, "<Field>@<verb>"} (generated/PipeFilled.inc:407-413), live at
every log level on purpose (PipeInputs.h:29-31: "this is not MOBILEGL_ASSERT, which is
inert in INFO builds").
The known BARRIER-PULLED rows — the 21 the reduced path actually reads
Union of kClear (7 of its 18 own fields), kDraw (19 of 47) and kReadback (12 of 17).
OpenRA adds no field to this set — it widens the site set, not the field set, and is the
first thing to reach the read-attachment sites (Managers.cpp:8603, :8966) and the
maxTouchedUnit >= 0 texture-unit walks.
| field | class | retires in | note |
|---|---|---|---|
GetBoundVertexArray |
O | P8 | DirectGLES.cpp:4486, PrepareForDraw, unconditional on every draw. PipeFill.cpp:1902-1905 says the pull retires at P8, not here. |
GetProgramForDraw |
O | P8 (Espryt), P7 (Magma) | DirectGLES.cpp:4497, same site, also unconditional. |
GetBufferBindingSlot |
O | P8 (indirect half), P9 (readback), P13 (transfer) | 18 Espryt sites; the 7 of 15 BufferTargets no call covers (Coverage.def:37-70). |
GetBufferBindingPoint |
O | P3b/P4b + P7 | |
GetTouchedBufferBindingPointCount |
V | P3b/P4b | |
GetFramebufferBindingSlot |
O | P3b/P4b (Espryt), P7 (Magma) | 8 Espryt sites through GetFramebufferBindingSlotChecked; SyncCurrentFBO (:2995) is self-declared monolith glue (DirectGLES.cpp:2961-2965) while BindCurrentFBO (:4303-4353) is already split-clean. |
GetTextureUnitObject |
O | P3b/P4b, P7 | 13 Espryt + 8 Magma sites. |
GetImageTextureBinding |
O | P3b/P4b, P7 | |
GetActiveTextureUnit |
V | P3b/P4b | server answers from its own state (Coverage.def:215-219). |
GetMaxTouchedTextureUnit |
V | P3b/P4b | hash-suppressed set, high-water mark still moves. |
GetTextureContextId |
V | P3b/P4b | not a value to migrate: the server answers from its own Serial. Coverage.def:220-224 is explicit that no call carries these three and none should. |
GetTextureBindGeneration |
V | P3b/P4b | as above |
GetSamplingResolutionGeneration |
V | P3b/P4b | as above |
GetCurrentVertexAttribute |
V | P3b/P4b | the applier cannot reproduce GLContext's cross-view conversion. |
GetPixelStoreParameters |
V | split in P5 — see below | |
IsTransformFeedbackActive, IsTransformFeedbackPaused, GetTransformFeedbackProgram, GetTransformFeedbackGeneration, GetBoundTransformFeedbackLifetimeId, GetTransformFeedbackCapturedVertices |
V/O | P3b/P4b (Espryt XFB scatter), P7 (Magma) | read on kDraw; XFB itself is off the reduced path but these are read on the draw walk regardless. |
GetPixelStoreParameters is split into pack and unpack in this phase (R-7's one small
migration, and it is c0's ruling to keep rather than soften). The field is m_pixelStore[2] and
only [0] (pack) has a carrier — which the applier does write (PipeApply.cpp:1373) — so the
whole field reads as unmigrated while the half a readback needs is fine. Splitting it is what
stops the readback path from taking a whole-field Fatal for a half it never touches. The five
Espryt read sites whose isUnpack argument decides which half they want are
DirectGLES.cpp:7924, :9399, :10893, :11272 and Utils.cpp:2302; the scout named them
and did not open them, so p1 reads the argument at each before it writes the two rows.
The FATAL rows
Three non-sticky fields, each off the reduced path for a different, checkable reason:
| field | why it is FATAL rather than pulled |
|---|---|
GetBoundTransformFeedbackName |
dead — read by no backend since D21 (PipeInputs.h:232-234). |
GetTransformFeedbackPausedPrimitiveCounter |
reachable only from class kQuery, which the reduced path never enters. |
GetProgramForDispatch |
reachable only from kDispatch; there is no compute on the reduced path. Sites: DirectGLES.cpp:5779, VulkanRenderer.cpp:7327, :7379. |
Plus MGPipeUnmigratedEmulation's five call sites, which in a split build stop being
(void)name; and become Fatal: Managers.cpp:5334 ("texture-remint-pull"),
DirectGLES.cpp:8051 ("generate-mipmap-storage"), :8702 ("generate-mipmap-cpu-fallback"),
:8997 ("copy-image-shadow-mirror"), :10623 ("get-tex-image-shadow"). One function grows
teeth and five sites get them — PipeApply.cpp:2820, today a no-op, which
PipeApply.h:1055-1066 and PipeApply.cpp:2812-2817 both already say is waiting for this
phase. None of the five is on the reduced path. (ROADMAP's P4a row says six sites; there are
five calls and one comment — Managers.cpp:5287 is the comment.)
The seven sticky forwards
GetBufferBindingPointCount, GetProgramObject, GetTextureObject,
HasOpenTransformFeedbackSpan, ValidateProgramName, InvalidateCompileEnv, RecordError
(PipeInputs.h:569-577; identity asserted :135-137; argued Coverage.def:138-150).
P5's assignment:
- the first five → BARRIER-PULLED (counted in
rsp,Fatalunder strict); InvalidateCompileEnv→OnCapsInvalidated, i.e. the re-arriving caps snapshot (R-12);RecordError→ BARRIER-PULLED, counted inrsp; its ordering is P9's (OnGlError).
And their exemption is cancelled in a split build. Today F-class accessors carry no
MGP_INPUT_CHECK at all (PipeInputs.h:563-568) and MGPipeInputFieldIsFresh
(generated/PipeFilled.inc:418-426) answers "fresh" for a sticky field regardless of
generation. That is exactly backwards for this phase: these seven are the ones that hand a
frontend object or a frontend write straight to the backend, so the exit gate "an
unmigrated field read is Fatal" is structurally blind on the seven most dangerous fields.
Under MOBILEGL_BUILD_DISAGGREGATED the exemption is lifted so they enter rsp, and under
MOBILEGL_IPC_STRICT_ERRORS=1 they become Fatal like any other BARRIER-PULLED row.
The prerequisite nobody else owns: someone must stamp
MGPipeApplyAccess deliberately does not stamp the poison generations
(PipeInputs.h:612-618): "a stamp says the filler published this for THIS verb, which is the
walk's statement, not the applier's". Under split the filler is in the other role, so nothing
stamps, every FilledGen[] stays 0, MGPipeInputFieldIsFresh returns false for everything,
and a purely-server-side read aborts on the first field inside SyncRenderState —
Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"} — long before any interesting
case. The server stamps at the verb boundary: p1 defines what is stamped and for which
verb, v1 places the call (Server/PipeApplier::StampVerbBoundary). Neither half works alone,
and this is not in the ROADMAP row.
Two sets with no field ids
The conservative client GPU-write set. One row per backend MarkGpuWritten site, mapped to
the client predicate that must fire, one unit case each:
| backend site | what it marks | when |
|---|---|---|
DirectGLES.cpp:570 |
every SSBO binding point | draw/dispatch about to go out |
DirectGLES.cpp:618 |
every bound atomic counter | every conformance case reads the increment back |
DirectGLES.cpp:2603 |
buffer textures on image units, only when Access != GL_READ_ONLY |
from draw preparation, deliberately not from glBindImageTexture |
UniformManager.cpp:1075 |
storage texel buffer, Access != GL_READ_ONLY |
after EnsureGpuResidentStorage() |
UniformManager.cpp:1231 |
SSBO block, unconditional | after EnsureGpuResidentStorage() |
VulkanRenderer.cpp:11618 |
the XFB capture targets | "the capture is a GPU write like any shader's" |
Plus two new producers P5 adds, both client-side with no server participation
(ARCHITECTURE.md:508): glReadPixels into a pack PBO becomes fire-and-forget plus a client
MarkGpuWritten (strictly better than monolith's unconditional stall), and
glEndTransformFeedback drops its unbounded fence wait and marks the capture targets instead.
SyncGpuWrites gains a third state it cannot express today — emitted, answer not back —
and under split it must block until OnBufferWriteback lands rather than clear the flag
optimistically (BufferObject.cpp:372-374 clears unconditionally; ARCHITECTURE.md:509 lists
this among the unavoidable blocking points, because monolith already glFinish()es here).
No narrowing in P5: ResourceTracker.h:587-592's rangeCount == 1 assertion stays.
The persistent-map reachable set: the census is 21 sites, not 20. MEASUREMENTS.md:111
records 20 and Managers.cpp:5047-5048 speaks of "the eleven Espryt" sites; the actual count
is 9 Espryt + 12 Magma = 21, and Espryt's own count is 12 (9 + 3 SyncGpuWrites), so both
published numbers are exactly one low and the missing one is an Espryt line. Ruling: 21 is
the number, and DirectGLES.cpp:361 (ResolveIndirectCommandBytes) is in the set. It is a
shared helper rather than a draw-path site, which is the most likely reason it was excluded,
but a helper that reaches a persistently-mapped range is exactly as able to read stale bytes as
a draw site is; excluding it would be a shutter that cannot see its own subject. The
per-site attribution table ARCHITECTURE.md:290 refers to as "§5.7" does not exist in the
tree — b1 should not go looking for it. The nine Espryt sites: DirectGLES.cpp:361, :6185,
:6439, :6440, :6541, :6542, Managers.cpp:2817, :2994, MultiDraw.cpp:511. The
twelve Magma: DirectVulkan.cpp:281, :472, :796, UniformManager.cpp:2024,
VulkanRenderer.cpp:3542, :3621, :4013, :7428, :12423, :12424,
VkBufferManager.cpp:628, :679. Correcting MEASUREMENTS.md:111 from 20 to 21 is b1's
line to write, in the same commit that lands the tracker.
m_livePersistentMaps is defined by SyncPersistentMappedRange's own early-out chain read as
a membership test (BufferObject.cpp:346-349): persistent, write, not flush-explicit,
not GPU-resident.
§4 Table 3 — role and thread ownership of every process global
ARCHITECTURE.md:578-581 claims MGPipe brings the globals a role split must duplicate down
from four to two. That census is at least seven short. Every row below also answers what
make-current and teardown do to it — the column ARCHITECTURE.md does not have.
| global | where | who writes | P5 ruling | make-current / teardown |
|---|---|---|---|---|
gPipeInputs (~20 KB) |
PipeInputs.h:706 |
client residual fill (PipeFill.cpp:530, :2131, :2604) + applier (PipeApply.cpp:1336, :1364, :1373, :1377, :1436, :1512) |
One instance is legal, but only under the verb barrier. The barrier makes at most one of {GL thread, apply thread} runnable at a time, so there is exactly one writer at any instant. No second writer may be introduced before the barrier retires. The invariant is a runtime assertion in debug/verify builds, not only a sentence here: the apply thread raises a flag on entering the applier and the client checks it when it touches gPipeInputs outside a barrier (ClientSession::InBarrierWait / ApplyThreadIsInsideApplier). |
make-current: unchanged. teardown: nothing — it is POD in the image. |
g_applier |
PipeApply.cpp:396 |
applier | Server-exclusive. Its own header already says "under split there is one per served context" (PipeApply.h:684). |
It is *new MGPipeApplierState{} and never destroyed, deliberately (PipeApply.cpp:392-395): resource_destroy is raised from ~BufferObject, which runs from exit handlers after this TU's globals are gone. |
g_resourceOps |
PipeApply.cpp:402 |
the backend, at register time | Server-exclusive, and the client must NEVER read it (R-8). Under inproc a client reading it is right by accident; under spawn it is null and the four P4a families plus P3a's buffers emit nothing at all, silently. The client asks CapsMirror::ServerConsumes instead. |
registered around DirectGLES.cpp:11933, nulled from OnBackendContextDestroyed (Managers.cpp:2584) — so it moves on every context loss, which is another reason the client cannot key on it. |
gMGPipeSegmentResolver |
MGPipeHostSpan.h:47 — a plain non-atomic inline variable |
MG_Remote installs |
One process-wide slot, so it cannot be per-role. Ruling: the server role installs it and the client never resolves a span at all — the client only ever writes Ptr = nullptr. SegmentTable::InstallProcessResolver() asserts if one is already installed, so two roles racing on it is loud rather than silent. Install before the apply thread starts. |
teardown: uninstall after the join, never before — a record still in flight can still resolve. |
the ten MG_Impl/Pipe *Instance() singletons |
fable-seam-audit.md:120-135 |
client | Client-exclusive. One correction carried forward: the texture drain list m_drain is process-wide, not per-context as D-D4 claims; the audit already booked "one drain per client context" as a P5 item. |
FreshlyPrimed (PipeFill.cpp:2414-2440) resets them on make-current; leak at exit (ID-8). |
ScopedDefaultUnpackState::s_synced + six value shadows |
Managers.cpp:5490-5496 |
backend | Server-exclusive — the sixth global the four→two census missed. Latent rather than live in P5 only because the client role never touches GL on the reduced path. (Managers.cpp:5491-5496 is six GLints, not five: s_skipImages at :5496 is the one usually dropped.) |
Never reset on context death — OnBackendContextDestroyed resets the rings and the binding caches and not this. Benign while a lost context returns the driver to GL defaults; not benign the day a server re-attaches to a context something else moved. Register it now. |
pActiveBackendObject |
GlobalObjects.cpp:23 |
MG_Backend::Init() |
Client installs BackendObject_Remote; the server's BackendObject_DirectGLES is held privately by ServerLoop. No thread-keyed shim, and therefore MOBILEGL_BUILD_DISAGGREGATED_INPROC needs none — but the cost is seven backend-internal reads across six functions, not the one the scout reported: BackendObject_DirectGLES.cpp:815, :819 (ClampSamplesToBackendSupport) and Utils.cpp:74, :82, :126, :220, :260. All seven are format-capability lookups, so "pass the format cache down" still works. DirectGLES.cpp:12446 is NOT ClampSamplesToBackendSupport — it is Present()'s fence poll, and DirectGLES.cpp contains no pActiveBackendObject reference at all. |
GetFormatCapabilities() is non-virtual (BackendObject.h:594), so the remote object must fill m_formatCapabilities rather than override the accessor. Teardown: pActiveBackendObject.reset() (MobileGL/Init.cpp:68) runs ~BackendObject_DirectGLES → DestroyEGLContext(), so under split it must be a blocking request onto the apply thread. |
gBackendFunctionsTable |
GlobalObjects.cpp:24, assigned Init.cpp:44 |
MG_Backend::Init() |
Client = the emit table (R-4); the server holds its real table directly and never goes through this global. | cleared at MobileGL/Init.cpp:91. |
Teardown order, ARCHITECTURE.md:537 plus the sentence it omits:
- client publishes and waits for the server to drain and acknowledge;
Doorbell::Kill()— the only thing that can wake an apply thread parked onkWaitForever(Doorbell.h:211-221; the shape is already pinned byInProcessTransportTest.cpp:344);- join, bounded (that test uses 5 s) so a regression is a red test and not a hung CI job;
- only then may the client free anything an emitter owns — a tail still referenced by an unapplied record is a use-after-free the join is what prevents;
- then the existing order (
MobileGL/Init.cpp:38-98).
ID-8 applies once per role-local singleton, not once overall: every new
MG_Remote/Client/* and MG_Remote/Server/* singleton leaks at exit. The proof recipe is
inherited: both lanes run GLIBC_TUNABLES=glibc.malloc.tcache_count=0.
Known open item, flagged not resolved. ARCHITECTURE.md:537's required order puts the
client's sync/query handle release after the transport closes, while today
DestroyAllSyncObjects / DestroyAllQueryObjects (MobileGL/Init.cpp:62, :67) deliberately
run before pActiveBackendObject.reset() (:68). The two are only reconcilable if a split
sync handle is client-minted and needs no backend call — which is P10's, not P5's. P5 keeps
today's order and v1 records which way it went.
§5 The knobs
Parsed in ConfigLoader.cpp, declared in Config.h. All of them live behind
#if MOBILEGL_BUILD_DISAGGREGATED — including the parser — because MG_ConfigLoader::Init()
is a pull-build symbol and G1 admits no resize, which is the same reason the
MOBILEGL_PIPE_VERIFY knobs sit behind their own #if.
| knob | default | notes |
|---|---|---|
MOBILEGL_TRANSPORT |
monolith |
monolith|inproc|spawn|unix:<path>|pipe:<name>. The three P6 forms parse and are then refused by name, staying on monolith: a P6 lane that set spawn, fell back silently and went green on the wrong arm is the failure this wording avoids. |
MOBILEGL_IPC_SERVER_PATH |
"" |
P6 consumes it; P5 parses it because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's SPLIT variant already carry it, and an unparsed variable is indistinguishable from a parsed-and-ignored one. |
MOBILEGL_IPC_RING_MB |
8 | SEG_CMD. One record may be at most half of this (RingProducer::MaxRecordBytes), so 8 MiB caps a record at 4 MiB. R-10 makes the codec publish a max-record-bytes counter rather than assume that is enough. |
MOBILEGL_IPC_STAGE_MB |
32 | SEG_STAGE. Every blob and every var-tail's bytes. |
MOBILEGL_IPC_SPIN_US |
50 | spin before parking, either direction. |
MOBILEGL_IPC_PERSISTENT_BLOCK_KB |
64 | 0 is the E3(a) negative control, not "unlimited": it turns the push off and PersistentCoherentMapScenario must go red. |
MOBILEGL_IPC_ADOPT_TIER |
2 | 2 = emulate, the only tier P5 implements. 0 and 1 parse and are Fatal at use, naming P11. |
MOBILEGL_IPC_VERB_BARRIER |
1 | 0 is R-1's negative control and is expected to be red. |
MOBILEGL_IPC_STRICT_ERRORS |
0 | promotes BARRIER-PULLED reads — and, in a split build, the seven sticky forwards — to Fatal. |
MOBILEGL_IPC_AUDIT |
0 | 0xDD over retired staging bytes (rule C's mechanical control). |
MOBILEGL_IPC_SERVER_AFFINITY |
auto |
kept as the raw string; whoever starts the apply thread logs the resolved mask, because an affinity that silently did nothing looks exactly like one that worked. |
One consequence, stated so it is not rediscovered. In a build without
MOBILEGL_BUILD_DISAGGREGATED, MOBILEGL_TRANSPORT=inproc is accepted by the environment and
silently ignored — the parser does not exist there, and putting a complaint in the
unconditional part of ConfigLoader would move a pull-build symbol and break G1. That is
precisely the shape of "the split lane ran monolith and went green", so the guard against it is
a build-level check, not a runtime one: nm --defined-only libMobileGL.so | grep -i MG_Remote must be non-empty in build-split, and it is t1's CI job to assert that.
CMake gained MOBILEGL_BUILD_DISAGGREGATED_INPROC (implies DISAGGREGATED) and, new here,
MOBILEGL_BUILD_DISAGGREGATED now implies MOBILEGL_PIPE_PUSH: the split path decodes
into the MGPipe applier and MOBILEGL_PIPE_PUSH is what compiles the applier, so
-DMOBILEGL_BUILD_DISAGGREGATED=ON alone used to configure cleanly and then fail to link — a
shape indistinguishable at the CMake level from a legitimate transport-only build.
§6 Rulings this file makes that the brief did not, and where the brief is wrong
Each entry says what would overturn it.
-
CallMaskbits 32..47 are the consumer mask. R-8 says the client's liveness gates read theCallMaskmirror, butCallMaskas declared has only nine feature bits and no per-family bit, so R-8 was not implementable as written. Overturned by: a decision to carry a second mask field inCapsSnapshotinstead — which costs a schema field and gains nothing, since 16 bits is enough through P8. -
tableSlotMaskis deleted, not renamed (R-8 allowed either). Decisive evidence:GLFunctionsTablehas 69 slots andulongis 64 bits, so the field cannot address the table its own comment names. Overturned by: widening the schema field and a reason to keep an explicit slot probe afterARCHITECTURE.md:114retired the concept. -
ResourceFlushRangecarries no bytes at all (R-13.2 offered "add a blobref" or "write the convention down"; this is a third answer, and a stronger one). The ladder it drives rewrites from the authoritative shadow, which under rule C is server-owned, soresource_subdatais already the only path bytes take. Overturned by: evidence that the tier-1INVALIDATE_RANGEarm needs bytes and range in one record. Cannot arise while the verb barrier holds; revisit when the barrier retires for the buffer family. -
ResourceRespecifyalso has a SECOND uncarried companion, and the brief does not mention it.const MGPRespecifiedLevel* level(PipeApply.h:792-795) is the scope of the redefinition andMGPResourceDesccannot express it. Without a carrier every per-levelglTexImage2Din OpenRA silently takes the whole-resource arm and drops every pending upload — the exact texel loss the server-side set exists to prevent. Ruling: two named fields in the existing pads, zero size change. c0 rules and specifies; the integrator lands theMGPipeTypes.h+PipeFields.defedit before w1 encodes this record. -
Table 1 is 23 rows, not 19. The brief's 19 and
scout-premortem:§3's 19 are different lists; the four only the premortem carries are the server → client ones, and a phase that omits them discovers in week three that it never decided where readback pixels land. -
SetResidualValueStateis a fourth typed companion, and neither scout nor the brief names it.MGPipeApplySetResidualValueStatetakesconst ResidualValueBlock&— not a payload, not aconst void*— andMGPResidualValueStateis never instantiated on the live path. The encoder has to invent both the record fill and the blob fill. Budget it as w1's hardest row, not as one of the easykHasBlobeight. -
Three of the brief's 19 have no applier entry point at all —
SetShaderBuffers(38),SetStreamOutputTargets(39),DrawVbo(59).scout-premortem:§3citesPipeApply.h:756, 941, 1028-1030for a six-call row; those five citations cover five other calls. For these three, "what crosses today" is nothing, and P5 writes the first producer and the first consumer. -
CreateRenderStateandSetDynamicStatedeclare aBlob.Sizethat nothing ever reads.scout-wire-codec:§4.2's "Size = 0" column is stale for four rows (addCreateVertexElementsandResourceSubData's buffer half). The applier's only fourBlob.Sizereads arePipeApply.cpp:702,:1998,:2353,:2784. A fifth flags-vs-payload-vs-signature disagreement for the reviewer's list. -
The emit table is 71 function pointers, not 69. R-4 says 69 slots; that is
GLFunctionsTable's count. The table the client actually installs isGlobalBackendFunctionsTable= 69 +Present+SetSwapInterval. R-4's rule (no null slot, no pass-through) applies to all 71, andPresentis on the reduced path. The Bool member is not a verb and is answered fromkCapCpuXfbPrimitiveAccounting. -
prefersCpuXfbPrimitiveAccountingis a member ofGLFunctionsTable(BackendObject.h:274), not ofDynamicBackendParameters. So it does not ride insideMGPCaps::Dynamic, and R-8's "same redundancy as (6)" is the wrong frame — it has three spellings and no carrier inMGPCapsexcept the cap bit. Its one non-test client reader isGL_Query.cpp:221. -
The persistent-map census is 21 sites, and
MEASUREMENTS.md:111's 20 is wrong. Ruling and the missing site named above.ARCHITECTURE.md's cited "§5.7" attribution table does not exist in the tree. -
ScopedDefaultUnpackStatehas six value shadows, not five (Managers.cpp:5491-5496); both the brief and the scout say five. -
CanTouchGLNow()guards 16 call sites, not 19. 19 is the raw grep: 1 definition (Managers.cpp:928) + 2 comment mentions (:1967,:1979) + 16 calls. The brief's "19 sites" over-counts.IsBackendContextCurrentOnThisThread's 16 is right. -
MGPipeApply*is 37 entry points and 41 call sites, not "~45 entry points" — andDirectGLES.cpp:12446isPresent()'s fence poll, notClampSamplesToBackendSupport(which isBackendObject_DirectGLES.cpp:807-828). Table 3'spActiveBackendObjectrow is still correct but the diff is six functions, not one line. -
The 18
build-splitunit aborts are not a poison problem. They areFatal{ProtocolCorruption}trip wires the tests expect; seven test TUs testMOBILEGL_PIPE_POISONwithout including the only header that defines it, so the macro reads as 0 and they compile the "logs and carries on" arm whilePipeApply.cppcompiles the aborting one. Invisible in a push build (where it really is 0) and in a verify build (where-DMOBILEGL_PIPE_VERIFY=1is on the command line);MOBILEGL_BUILD_DISAGGREGATEDis the one arming condition behind the header. Fixed in c0's own commit, test-local, no p1 surface. -
MGHostSpan, notMGPHostSpan. The header isMGPipeHostSpan.h; the struct isMGHostSpan(:28).MGPHostSpandoes not exist. -
ARCHITECTURE.md's own corrections, carried here so they are not lost::83says 61PipeInputsfields, it is 63;:492citesPipeStats.h:126forMapPersistentRoundtrips, it is:141; ROADMAP's P4a row says sixMGPipeUnmigratedEmulationsites, it is five calls plus one comment;ARCHITECTURE.md:19says eight EGL lifecycle virtuals, there are nine (ResizeEGLWindowSurfaceis the uncounted one).