38 KiB
P5 scout — the wire codec (G3) and the call catalogue's flags
Tree: /home/swung/w7/pipe @ a29807cc (P4a landed). Read-only survey; nothing was built or run.
All line numbers are from that commit.
0. Headline (the three things the brief writer needs first)
- There is no codec. G3 emits no encoder and no decoder — only record structs, an
opcode enum, size
static_asserts and a bounds gate whose every arm isreturn false. Nothing outsideMG_Test/Pipe/PipeCatalogueTest.cpptouches a single G3 symbol. P5 writes the codec from zero; the only thing G3 hands it is the opcode numbering and the 8-byte header shape. - The monolith never fills a blob. Every
kHasBlobrecord on the wire today declaresBlob.Size = 0("this record does not declare its blob") and the bytes travel as a companion pointer beside the record — and for three records that companion is not bytes at all but a typed frontend pointer (const SamplerParameters*,const LinkArtifacts*+const SpirvArtifacts*). A split emission has to invent the serialisation for those, and for the two payloads that carry anMGPBlobRefbut are not flaggedkHasBlob(MGPCaps,MGPSamplerDesc). - 34 of the 71 rows have no applier entry point at all — not "an applier nobody
calls". Exactly one row matches that description (
UnmapPersistent, deliberately producer-less,PipeFill.cpp:764-771); the other 36 wired rows all have live callers. So P5's first-exercise set is 34 rows of brand-new applier surface (draws, clears, blits, present, fences, queries, readback) plus one dormant producer, not 34 rows of dormant code.
1. What scripts/gen_pipe.py's G3 generator emits today
1.1 Generator and output
- Generator:
scripts/gen_pipe.py:605def gen_wire(calls, residual_fields=None). - Single output:
MobileGL/MG_Pipe/generated/PipeWire.inc(939 lines), written atscripts/gen_pipe.py:1275-1277. - Banner (
scripts/gen_pipe.py:606-607): "G3: wire records, size assertions and the applier's bounds gate." That title is exhaustive — those three things are all it emits.
1.2 The five things it actually emits
| # | Emitted | gen_pipe.py | PipeWire.inc |
|---|---|---|---|
| 1 | struct MGPWireRecHeader {Uint16 Op; Uint16 Flags; Uint32 Size;} + sizeof == 8 + trivially-copyable asserts |
:628-634 |
:35-41 |
| 2 | enum class MGPWireOp : Uint16 — kInvalid = 0, one enumerator per catalogue row at its 1-based position, kOpCount = 72 |
:638-644 |
:45-119 |
| 3 | 71 × struct alignas(8) MGPWireRec_<Name> { MGPWireRecHeader Header; <Payload> Payload; }; each with a composition size assert sizeof(rec) == ((8 + sizeof(payload) + 7) & ~7) |
:645-652 |
:121-687 (GetCaps at :121, FenceWaitServer at :681) |
| 4 | [[noreturn]] MGPipeWireProtocolFatal(call, size, remaining) → MGLOG_F + std::abort(), and macro MGP_WIRE_CHECK_BOUNDS(RecType, CallName) asserting size >= sizeof(RecType) && size <= remaining && (size % 8) == 0 |
:653-664 |
:689-701 |
| 5 | inline Bool MGPipeApplyWireRecord(MGPWireOp, const void* record, Uint64 size, Uint64 remaining) — a 71-arm switch, every arm does MGP_WIRE_CHECK_BOUNDS(...); return false;, default/kInvalid/kOpCount → fatal |
:666-684 |
:708-933 |
Plus, when PipeFields.def gives a ResidualValueBlock field list, a tail of
offsetof asserts pinning that migration carrier's layout (gen_pipe.py:686-708,
PipeWire.inc:935-941). Today the block has exactly one member (CapabilityBits) so only
the offset-0 and sizeof == MGL_RESIDUAL_BLOCK_SIZE asserts survive.
Encoder: none. Decoder: none. There is no Encode<Call>, no Decode<Call>, no tail
walker, no blob packer, no MGPWireRecHeader writer. record is (void)-cast at
gen_pipe.py:673 / PipeWire.inc:709 — the skeleton never dereferences the payload.
The generator says so itself (gen_pipe.py:666-671, verbatim in PipeWire.inc:702-707):
"Returns whether the record was applied. P0 is a SKELETON: every case validates its bounds and then reports 'not applied', because no applier exists until P5 wires
MG_Remote/Server/PipeApplier.cppto the real backend tables."
1.3 Who includes it — one file, and one test uses it
MobileGL/MG_Pipe/MGPipe.h:145#include "generated/PipeWire.inc"(insidenamespace MobileGL::MG_Pipe). That is the only includer.- The only code that references any G3 symbol is
MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp:404-432and:903(WireOpcodesAreThePositionsInTheCatalogue,LateArrivalsAreAppendedWithoutRenumbering,ApplierAcceptsAWellFormedRecord).MG_Remote/references zero G3 symbols;MG_Impl/andMG_Backend/likewise. One prose mention only:MobileGL/MG_Pipe/MGPipeTypes.h:22("these structs (generated/PipeWire.inc) are memcpy'd"). - Same story one layer out: G1's two tables
gMGPipeScreen/gMGPipeContext(MGPipe.h:137-138) and G2'sMGP_<Call>thunks (generated/PipeThunks.inc) are also dead — the only reference isPipeCatalogueTest.cpp:107,111reinterpreting them as pointer arrays to count entries. The monolith's real dispatch is the directMGPipeApply*call and theMGPipeResourceOpstable (PipeApply.h:85-98), not the generated tables. (Guess: P5 may find it cheaper to keep dispatching throughMGPipeApply*on the server side and use the tables only as the emitter's shape, rather than to finally install G1/G2 — but that is a design call, not something the tree decides.)
1.4 The chunking contract, quoted
scripts/gen_pipe.py:618-631 (identical text at PipeWire.inc:25-33):
// OVERSIZED PAYLOADS ARE CHUNKED, NEVER EMITTED WHOLE (plan section 8.2: G3 has to define// the path for a record larger than the segment). The bound is the ring's,// RingProducer::MaxRecordBytes() == Capacity()/2, and it is exact rather than// conservative: a record has to be placeable at every head offset of an empty ring, the// wrap pad in front of it costs up to total-8 bytes, and only a record of at most half the// ring survives that at every offset. An emitter holding more than Capacity()/2 bytes of// record (a large resource_subdata, a create_shader_state archive) splits it into several// records of at most that size; the transport refuses a bigger one outright - nullptr plus// an MGLOG_E - rather than let the producer wait on free bytes that can never suffice.
Precisely, the contract is:
- Bound =
RingProducer::MaxRecordBytes() == Capacity()/2(MobileGL/MG_Remote/Transport/Ring.h:195), measured on header + payload rounded up to 8, not on the payload alone (Ring.h:181-191). - Refusal is not backpressure.
Reservereturnsnullptrhowever empty the ring is for an over-size record, with an error log;Ring.h:186-191spells out the disambiguation: "nullptr withFreeBytes() >= totalnever means 'wait'; it can only mean 'too big, chunk'." - Splitting is the emitter's job, explicitly ("chunking it is the emitter's job (plan
section 8.2, the G3 chunking rule)",
Ring.h:182-183). The generator names the two expected victims: a largeresource_subdataand acreate_shader_statearchive. - The chunk protocol itself does not exist. There is no continuation flag, no chunk
index, no reassembly buffer, no per-op "this is chunk k of n" anywhere in
PipeWire.inc,MGPipeTypes.horRing.h.MGPWireRecHeader.Flagsis documented only as "MGPipeCallFlags of the call, for asserts and tracing" (PipeWire.inc:37) — there is no reserved chunk bit. - ROADMAP puts "G3 分块路径" (the G3 chunking path) in P8, not P5
(
docs/Disaggregated/ROADMAP.md:25). Two readings, and the brief must pick one explicitly: (a) P5 ships the reduced path and simply refuses (Fatal) any record aboveMaxRecordBytes(), leaving chunking to P8; (b) P5 must chunkcreate_shader_statefrom day one because a real program archive can exceed 4 MiB against a default 8 MiBSEG_CMD. Evidence that would settle it: thecsob-blobbyte counter already published in P4a —INTEGRATOR-DECISIONS.md:803records DirectGLES median 2928 B/frame, rd12 peak 1.06 MB/frame. A 1.06 MB per-frame aggregate is under a 4 MiB cap, but that is frame totals, not the largest singleCreateShaderStaterecord; a per-record histogram from aMOBILEGL_PIPE_STATSrun would settle it.scout-frontend-state.md:922(P4a scout) already flagged this as risk 8. - Same bound is restated in
docs/Disaggregated/ARCHITECTURE.md:125.
2. The 71 rows by flag class
Parsed from MobileGL/MG_Pipe/PipeCalls.def:78-166. Flag semantics are at
MobileGL/MG_Pipe/MGPipe.h:39-54. Opcode = 1-based position; verified against
PipeWire.inc:45-119.
Totals: kHasBlob 8, kVarTail 9, kHostSpan 2, kReplySlot 10, kNeedsAck 1,
kOptional 4. Unique flagged rows 29; kNone 42; 29 + 42 = 71. ✔
2.1 kHasBlob — 8 rows
| op | call | payload | class | def line |
|---|---|---|---|---|
| 17 | CreateRenderState |
MGPRenderStateDesc (48) |
kCtxCso | :98 |
| 20 | CreateVertexElements |
MGPVertexElements (40) |
kCtxCso | :101 |
| 27 | CreateShaderState |
MGPProgramDesc (192) |
kCtxCso | :108 |
| 30 | SetDynamicState |
MGPDynamicState (32) |
kCtxState | :112 |
| 40 | SetGlobalConstants |
MGPGlobalConstants (40) |
kCtxState | :122 |
| 46 | SetResidualValueState |
MGPResidualValueState |
kCtxState | :131 |
| 48 | ResourceSubData |
MGPSubData (72) |
kCtxObject | :134 (also kVarTail) |
| 49 | BufferSubDataResident |
MGPSubData (72) |
kCtxObject | :135 (also kOptional) |
What the encoder must do beyond memcpy of the POD: copy Blob.Size bytes from the
companion pointer into SEG_STAGE (or inline, ≤4 KiB, into SEG_CMD —
ARCHITECTURE.md:409), then fill in Blob.{Seg, Offset, Size} — fields the monolith
leaves at {kMGHostSpanSegNone, <a raw host address>, 0}. The decoder must resolve
Seg/Offset back to a readable pointer and hold the record to its declared length (§4).
Two payloads carry an MGPBlobRef but are NOT flagged kHasBlob — a real trap:
MGPCapscarries two:FormatCapabilitiesandRendererInfo(MGPipeTypes.h:137-138), andGetCapsiskReplySlotonly (PipeCalls.def:80). The header itself defers them to P5: "Their serializers land with the transport (P5)" (MGPipeTypes.h:135-136). P5's charter includes "MGPCaps 快照", so this is directly on the critical path: the format-capability cache holdsVector<Int>sample-count lists and the renderer strings areStrings — neither is a POD.MGPSamplerDesc::Parameters(MGPipeTypes.h:429), andCreateSamplerStateiskNone(PipeCalls.def:104). The P4a scout already flagged the asymmetry (~/w7/notes/p4a/scout-pipe-and-client.md:106). The applier takes it as a typed pointer,const SamplerParameters* parameters(PipeApply.h:1000), not bytes.- Contrast:
ResourceRespecifydeliberately does not carrykHasBloband that is argued —MGPResourceDeschas noMGPBlobRefmember, so the flag would be a lie (PipeApply.h:76-79). SokHasBlobmeans "owns anMGPBlobRefmember", which is exactly the invariant the two rows above break. - And
kHasBlobis singular while the payload may hold seven:MGPProgramDeschasMGPBlobRef Spirv[6]+MGPBlobRef Reflection(MGPipeTypes.h:507-508). One flag, seven independent byte runs on one record.
2.2 kVarTail — 9 rows
| op | call | payload | tail layout (source) |
|---|---|---|---|
| 32 | SetVertexBuffers |
MGPVertexBuffers |
MGPVertexBuffer[] (PipeApply.h:941) |
| 35 | SetSamplerViews |
MGPSamplerViews |
MGPBoundView[] (PipeApply.h:1028) |
| 36 | BindSamplerStates |
MGPSamplerStates |
MGPipeHandle[] (PipeApply.h:1029) |
| 37 | SetShaderImages |
MGPShaderImages |
MGPImageView[] (PipeApply.h:1030) |
| 38 | SetShaderBuffers |
MGPShaderBuffers |
MGPBufferRange[Count] then MGHostSpan[HostSpanCount] (MGPipeTypes.h:820-822) |
| 39 | SetStreamOutputTargets |
MGPStreamOutputTargets |
MGPBufferRange[Count] then Uint32 offsets[Count] (MGPipeTypes.h:836) |
| 41 | SetVertexAttribDefaults |
MGPVertexAttribDefaults |
MGPAttribValue[] (PipeApply.h:756) |
| 48 | ResourceSubData |
MGPSubData |
MGPSubRegion[RegionCount] (MGPipeTypes.h ~:1005) |
| 59 | DrawVbo |
MGPDrawInfo (56) |
MGPDrawRange[NumDraws], plus an MGHostSpan only when kDrawHasUserIndices (MGPipeTypes.h:1240,1219) |
What the encoder must do beyond memcpy: compute the total record size from the header's
own count fields, reserve header + fixed + tail, and write two or three arrays back to
back (SetShaderBuffers and SetStreamOutputTargets each have two tails; DrawVbo has a
conditional second tail). The decoder must recompute the same arithmetic and check it
against MGPWireRecHeader.Size — note MGP_WIRE_CHECK_BOUNDS only proves
size >= sizeof(MGPWireRec_X), i.e. it does not see the tail at all; a kVarTail
record that declares Count = 4000 and carries 8 bytes passes today's gate. Every tail
bound today comes from a hand-written check inside PipeApply.cpp (e.g.
kMGPipeMaxVertexAttribs at PipeApply.cpp:1993-1997).
2.3 kHostSpan — 2 rows
SetShaderBuffers (op 38) and DrawVbo (op 59). Both are also kVarTail; the host span
is a tail element, never inline. See §3.
2.4 kReplySlot — 10 rows
op 1 GetCaps, 5 MapPersistent, 8 FenceStatus, 9 FenceWait, 14 QueryAvailable,
15 QueryResult, 52 ResourceReadback, 55 GetTextureImage, 58 ReadPixels,
69 QueryTimestamp (PipeCalls.def:80,84,87,88,94,95,138,141,145,160).
What the encoder must do beyond memcpy: mint a reply-slot id (MGPReplySlot{Uint64 Id},
MGPipeTypes.h:79-81), record the pending answer client-side, and arrange for the server to
write back into SEG_REPLY (8 MiB, 4 KiB slots — ARCHITECTURE.md:411) and post an
EvReadbackDone/EvQueryResult/EvFenceSignaled on SEG_EVENT. The catalogue's stated
rule is that none of these block — "Every server query in this catalogue is
async-with-handle; none of them blocks (section 4.4.6, 'the total rule')"
(MGPipeTypes.h:77-78). P5's charter contradicts that for one row: ROADMAP P5 says
"阻塞 read_pixels" (blocking read_pixels, ROADMAP.md:21), and SEG_REPLY is P9
(ROADMAP.md:29). So in P5 ReadPixels is a synchronous round trip that does not use its
reply slot the way the type comment describes. Flag that as an explicit deviation to write
down, not an accident.
Note also that 7 of these 10 rows have no applier at all today (§5) — only
MapPersistent (PipeApply.h:917), ResourceReadback (:908) and nothing else from this
list exists.
2.5 kNeedsAck — 1 row
Only ResourceRespecify (op 3, PipeCalls.def:82). It is a per-record predicate, not
per-call: MGPipeResourceRespecifyNeedsAck(desc) (MGPipeTypes.h:680, per P3a notes it is
== (desc.Immutable != 0)), so the same call carries every glBufferData without acking
one and only glBufferStorage blocks (PipeCalls.def:20-24). What the encoder must do:
publish, ring the doorbell unconditionally (ARCHITECTURE.md:464 lists "any kNeedsAck
request" as an explicit doorbell point), then spin MOBILEGL_IPC_SPIN_US → set
producerParked → block, and the decoder side must post the ack watermark.
2.6 kOptional — 4 rows
op 5 MapPersistent, 6 UnmapPersistent, 49 BufferSubDataResident, 68 SetSwapInterval
(PipeCalls.def:84,85,135,155). Semantics (MGPipe.h:51-53): a null table entry is a
real answer, not an error — DirectVulkan deliberately leaves buffer_subdata_resident
unregistered. What the encoder must do: the null check cannot live on the server. If the
client emits a record for a call the remote backend does not implement, the bytes are spent
and the frontend has already taken the "it happened" branch. The client therefore needs the
remote's per-call implemented mask before it emits — which is what MGPCaps::CallMask
(MGPipeTypes.h:133, MGPCapBit) is for. P4a learned this the hard way as the "consumer
gate" (ROADMAP.md:23: four families had to check that some backend registered
MGPipeResourceOps, because Magma had no P4a twin and the client cleared dirty flags on
acceptance, costing 66 DirectVulkan cases their uploads). P5 must wire CallMask into the
emitter before the first kOptional record goes out.
2.7 The flags are not available at runtime
MGPWireRecHeader.Flags is documented as carrying "MGPipeCallFlags of the call"
(PipeWire.inc:37) — but no generator emits a per-opcode flags table. Grep for
kHasBlob|kVarTail|kHostSpan|kReplySlot|kNeedsAck|kOptional across
MobileGL/MG_Pipe/generated/ returns nothing. PipeTables.inc is function pointers only.
So a P5 decoder that wants to ask "does opcode 48 carry a blob?" has nowhere to ask, and an
encoder that wants to stamp Flags has to hard-code it. This is a one-line addition to
gen_pipe.py (a kMGPipeCallFlags[kOpCount] array out of gen_tables or gen_wire) and
the brief should call for it explicitly, because otherwise each of the ~6 emitter
packages will hard-code its own copy.
3. MGHostSpan / MGPHostSpan
The name in the tree is MGHostSpan, not MGPHostSpan — the header is
MobileGL/MG_Pipe/MGPipeHostSpan.h but the struct at :28 is MGHostSpan (no P).
MGPHostSpan does not exist; the brief should use MGHostSpan.
3.1 What it is
struct MGHostSpan { // MGPipeHostSpan.h:28-37, 32 bytes
const void* Ptr; // monolith: the frontend shadow or the app's own memory
Uint32 Seg; // split: a SEG_STAGE id, or a sentinel
Uint32 Pad0;
Uint64 Size;
Uint64 Offset;
};
MGPipeHostSpan.h:12-13: "The ONE thing in MGPipe whose shape changes with the transport." It is the only place in the whole interface where a pointer is allowed (MGPipeTypes.h:17-19), and even then only in a variable tail, never inline in a fixed payload — so the VBO path (every Minecraft/Sodium draw) never pays its 32 bytes (MGPipeTypes.h:1226-1227).- Field order is chosen so the struct is 32 bytes on both 64-bit and 32-bit hosts
(
MGPipeHostSpan.h:29-31) — do not reorder. - Two sentinels (
MGPipeHostSpan.h:21,26):kMGHostSpanSegNone = 0andkMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF("the bytes are already on your side, read them out of the index host mirror"). - Resolution is one predictable branch,
MGPipeHostBytes()(MGPipeHostSpan.h:50-56):Ptr != nullptr→Ptr + Offset; else callgMGPipeSegmentResolver(Seg, Offset, Size); elsenullptr. gMGPipeSegmentResolver(MGPipeHostSpan.h:46-47) is a plain function-pointer global,nullptrtoday, to be installed byMG_Remote(:42-45).PipeCatalogueTest.cpp:702currently asserts it is null.
3.2 Which calls carry one
Exactly two, both kHostSpan|kVarTail:
SetShaderBuffers(op 38).MGHostSpan[HostSpanCount]is an optional second var-tail behindMGPBufferRange[Count].HostSpanCountis0, orCountfor theUniformclass underkCapNeedsHostUboBytes— "a range with nothing to ship carries an empty span, so the two arrays stay index-aligned" (MGPipeTypes.h:820-823,:806-811, cap bit at:122). Rationale for not inlining it: it would have cost every SSBO / atomic-counter / XFB range 32 dead bytes, and D-B8 says not to freeze the shape before thestage-ubo-namedcounter has numbers (MGPipeTypes.h:806-811).DrawVbo(op 59). OneMGHostSpanfor the user index bytes, present only whenFlags & kDrawHasUserIndices(MGPipeTypes.h:1211,1219).
3.3 What a split build has to do with it
Nothing today constructs an MGHostSpan outside tests. Grepping the type (excluding the
kMGHostSpanSeg* constants) finds only MGPipeHostSpan.h itself and
PipeCatalogueTest.cpp:626,627,693,699. The kMGHostSpanSegNone constant is used widely —
but as the Seg value of an MGPBlobRef, a different 24-byte struct
(MGPipeTypes.h:55-61): e.g. ResourceTracker.h:215, TextureEmit.h:1265,
ProgramEmit.h:249,253, VertexInputEmit.h:396, SamplerEmit.h:433,
Managers.cpp:2138-2139. Do not let the brief conflate the two structs.
For P5 specifically:
- Both
kHostSpanrows (SetShaderBuffers,DrawVbo) are in the 34 rows with no applier and no emitter (§5). So P5 is not "filling in the split branch of an existing host span" — it is writing the first producer of one, on the reduced path (ClearThenReadPixels,Triangle, an OpenRA trace). ARCHITECTURE.md:122states the split fill:Ptr == nullptr, bytes inSEG_STAGEnamed bySeg/Offset, orSeg == kMGHostSpanSegFromServerIndexMirror.HostResolveis not P5.MG_Impl/Pipe/HostResolve.cpp(client array ranges, max-index scan,*IndirectCountresolution, each with a per-site reconcile) and theMGHostSpansplit fill andServer/IndexHostMirrorare all P8 (ROADMAP.md:25; the directory listing atARCHITECTURE.md:569marksHostResolveas[P2+]but no such file exists —ls MobileGL/MG_Impl/Pipe/shows 15 files andHostResolve.*is not among them).ARCHITECTURE.md:398-404is the index-host-mirror section, also labelled P8.- Practical consequence for P5: the reduced path must either not emit a
kDrawHasUserIndicesdraw at all, or installgMGPipeSegmentResolverand stage the index bytes intoSEG_STAGEby hand. ATriangletest with a VBO-backed draw avoids it entirely.kCapNeedsHostUboBytesandkCapNeedsHostIndexBytes(MGPipeTypes.h:119,122) are the two cap bits that arm the host-span path; leaving both clear in P5'sMGPCapssnapshot is the cheapest way to keepMGHostSpanout of the first IPC frame. (That is my reading of the cap bits' purpose; it is not stated as P5 guidance anywhere — guess.)
4. The blob rule, and what a split emission has to change
4.1 The rule, stated twice in the header
MobileGL/MG_Pipe/MGPipeTypes.h:398-410 (on MGPVertexElements) and again at :985 (on
MGPSubData), restated in the applier at PipeApply.cpp:1982-1989:
// THE BLOB RULE - ONE RULE FOR EVERY RECORD IN THIS HEADER THAT CARRIES AN MGPBlobRef,// and MGPSubData below is the other one.Blob.Sizeis the record's own statement of how// many bytes its blob holds, and the applier holds the record to that statement WHENEVER// THE RECORD MAKES IT: a non-zero Blob.Size that disagrees with the byte length the// record's other fields describe is Fatal{ProtocolCorruption} and the call is refused. A// ZERO Blob.Size means "this record does not declare its blob", which is what a monolith// emission is - the bytes travel beside the record through the apply entry point's// companionconst void*and no MGPBlobRef is filled - and it is not a fault.
And the load-bearing half (MGPipeTypes.h:407-410): "Either way the bytes read are bounded
by the record's OTHER fields (the two counts here, the destination range there), so the
length is a cross-check and never the safety property."
~/w7/notes/p4a/BRIEF-P4A.md:967 calls it "the one Blob rule (ID-8b)";
p4a-results/clientsp-v1.md:144 confirms the P4a landing: "All seven MGPBlobRefs declare
Seg = kMGHostSpanSegNone, Size = 0 (the one Blob rule)."
4.2 What a monolith emission puts in MGPBlobRef today
For every blob-carrying record: Seg = kMGHostSpanSegNone (0), Size = 0,
Offset = a raw host address (the client's own shadow base), and the bytes are handed to
the applier as a separate argument. Concretely:
| record | MGPBlobRef filled as |
the bytes arrive as | citation |
|---|---|---|---|
ResourceSubData (texture) |
Seg = None, Offset = address of level shadow base, Size = 0 |
const void* bytes |
TextureEmit.h:1260-1265; BRIEF-P4A.md:560 |
ResourceSubData (buffer) |
same shape | const void* bytes |
ResourceTracker.h:203,215 |
CreateVertexElements |
Blob.Seg = None, Size = 0 |
const void* blobBytes |
VertexInputEmit.h:396; PipeApply.h:932 |
CreateRenderState / SetDynamicState |
Size = 0 |
const void* chunkBytes |
PipeApply.h:740,748 |
SetGlobalConstants |
Size = 0 |
const void* bytes = MapUBO()'s image, GetUBOSize() long |
ProgramEmit.h:192; PipeApply.h:1047-1052 |
CreateSamplerState |
Parameters.Seg = None, Size = 0 |
const SamplerParameters* — a typed frontend struct, not bytes |
SamplerEmit.h:433; PipeApply.h:1000 |
CreateShaderState |
all seven (Spirv[6] + Reflection) Size = 0 |
const LinkArtifacts* + const SpirvArtifacts* — two typed frontend objects |
ProgramEmit.h:249,253; PipeApply.h:1032-1041 |
MGPipeApplyCreateShaderState's own comment (PipeApply.h:1032-1039) is the cleanest
statement of the situation, and it names P5 by name:
"THE ARTEFACTS TRAVEL BESIDE THE RECORD, by pointer: all seven of
desc.Spirv[]anddesc.Reflectionare declared with Size 0 … and the codec is NOT called — zero serialisation cost on the monolith path. … Splitting this record for a transport whose ring caps one record at half its capacity is P5's problem, not this entry point's."
MGPipeResourceOps' header says the same thing generally (PipeApply.h:73-79): "in
monolith a blob needs no MGPBlobRef and the pointer is the client's own shadow base, so
the call is zero-copy and behaviour is unchanged. How those bytes cross under a real
transport is that phase's problem and that phase's flag edit."
4.3 What a split emission has to put there
- A real
Seg— aSEG_STAGEid (or aSEG_CMDinline id for ≤4 KiB payloads,ARCHITECTURE.md:409) — instead ofkMGHostSpanSegNone. - A real
Offset— a byte offset into that segment, not a host address. The monolith'sOffsetis areinterpret_cast<Uint64>(pointer)(Managers.cpp:2138) and is meaningless across the line. - A real, non-zero
Size— which arms the rule's Fatal arm for the first time. Today every cross-check inPipeApply.cppis inert by construction (p4a-results/wire-v1.md:140: "so the one Blob rule has nothing to cross-check and is inert here by construction"). P5 is where those gates go live, so P5 is also where a stale or off-by-one length in any emitter becomes a hard abort. The known per-record cross-checks the server will start enforcing:MGPSubDatabuffer half must equalMGPipeSubDataBufferSize(record)(wire-v1.md:320);MGPVertexElementsmust equalAttributeCount*sizeof(MGPVertexAttribWire) + BindingPointCount*sizeof(MGPVertexBindingPointWire)(PipeApply.cpp:1990-1998);MGPGlobalConstantsmust equalDesc.GlobalUboSize(wire-v1.md:192);MGPSamplerDescmust equalsizeof(SamplerParameters)(wire-review-v1.md:277). - A serialiser for the three typed-pointer companions. Two of the three already exist,
one does not:
CreateShaderState:EncodeProgramArtifacts/DecodeProgramArtifactsalready exist —MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.{h:53,60,cpp:252,264}, with a dedicated test suiteMG_Test/Program/ProgramArtifactsCodecTest.cppcovering truncation, trailing garbage, version and size mismatch. Better still, a verify build already runs the round trip over every real program the verify lane links:PinProgramArchiveRoundTrip(PipeApply.cpp:1056-1087), called at:2638. It also pins the one member the codec must never carry: a decode that reconstructed a live glslangTProgramis a fault (PipeApply.cpp:1073-1078). This is P5's single biggest piece of free work — the program archive codec is written, tested and already validated against production programs.CreateSamplerState:SamplerParametersis a POD value type inMG_Pipe/MGPipeValueTypes.h, so amemcpyofsizeof(SamplerParameters)is the serialisation, andborderColorFormmust survive byte-for-byte (MGPipeTypes.h:425-428). Cheap.MGPCaps: no serialiser exists.FormatCapabilitiesholdsVector<Int>sample-count lists andRendererInfoholdsStrings (MGPipeTypes.h:135-138), and the header explicitly defers "Their serializers land with the transport (P5)".MGPCapsalso embedsDynamicBackendParameterswhose literal size is ABI-dependent (MGPipeTypes.h:143-147asserts the size as a composition, not a literal, and says fixed-width members are P1/P7 work). Forinprocboth sides share the ABI so this is latent; for P6'sspawnit is not. Say so in the brief:MGPCapsis the one record whose wire form is not yet ABI-stable, and P5 should either pin the ABI or accept thatspawn(P6) inherits aSegmentMismatch/AbiMismatchrisk.
- Seven blob refs on one record.
CreateShaderStateneeds seven independent{Seg, Offset, Size}fills from one archive, and it is also the record most likely to exceedMaxRecordBytes()(§1.4).
5. Rows never emitted by the client in monolith, as of a29807cc
5.1 Correction to the question's premise
Exactly one call has an applier entry point with no production caller —
UnmapPersistent, and it is deliberate (below). Everything else splits into "applier +
emitter, both live" (36 rows) or "neither exists" (34 rows). I enumerated all
MGPipeApply* symbols in PipeApply.{h,cpp} (37 call-shaped entry points plus the helpers
MGPipeApplier/MGPipeApplyAccess) and counted references for each outside
MG_Pipe/PipeApply.*: every one has ≥ 1. Two are worth noting because they have no
MG_Impl caller at all in the grep:
-
MGPipeApplyUnmapPersistent(op 6) — the one row that genuinely matches the question's premise: the applier exists and nothing in production calls it. Callers are onlyMG_Test/Pipe/ResourceEmitTest.cpp:462,535,560,926,1648. This is deliberate and argued atMG_Impl/Pipe/PipeFill.cpp:764-771:"NO UnmapPersistent PRODUCER IN P3a, AND THAT IS DELIBERATE. The catalogue has the call and wire implemented it, but D-J forbids new behaviour and there is nothing to convert:
BufferBackendOpshas seven hooks and none of them is an unmap, andPipeResource::ReleasePersistentMap()(BufferObject.cpp, fromRedefineStorage) tells the backend nothing today — it learns from theRespecifythat follows. … The producer lands with the phase that gives the backend an unmap hook."The backend side is already there (
Managers.cpp:2250Ops_H_UnmapPersistent, registered at:2307; dispatched atPipeApply.cpp:1944-1945). P5 is plausibly that phase — its charter is client-side block-granularity persistent-map push andPersistentCoherentMapScenariogreen — so the brief should decide explicitly whether P5 lights this producer up or leaves it to P11. (Guess: P5 needs it, because under split a persistent map is a server-side mapping that the client cannot silently let aRespecifyclean up the way a shared address space does.) -
MGPipeApplyBufferSubDataResident— exactly one production caller,MG_Impl/Pipe/PipeFill.cpp:727.
5.2 The 34 rows P5 is the first to exercise — no applier, no emitter
71 − 37 = 34. These are new surface on both sides.
Screen (6):
GetCaps (1), FenceCreate (7), FenceStatus (8), FenceWait (9), FenceDestroy (10),
FenceWaitServer (71).
Context / query (8):
QueryCreate (11), QueryBegin (12), QueryEnd (13), QueryAvailable (14),
QueryResult (15), QueryDestroy (16), QueryTimestamp (69), QueryCounter (70).
Context / state (3):
SetIndirectBuffers (34), SetShaderBuffers (38), SetStreamOutputTargets (39).
Context / object (4):
ResourceSubDataComplete (50), ResourceCopyRegion (53), GenerateMipmap (54),
GetTextureImage (55).
Context / verb (13):
Blit (56), Clear (57), ReadPixels (58), DrawVbo (59), LaunchGrid (60),
MemoryBarrier (61), BeginStreamOutput (62), EndStreamOutput (63),
PauseStreamOutput (64), ResumeStreamOutput (65), Flush (66), Present (67),
SetSwapInterval (68).
Cross-check: ls MobileGL/MG_Impl/Pipe/ holds 15 files —
CompositeResolver.h CsoCache.h FramebufferEmit.h ImageEmit.h PipeFill.{cpp,h} ProgramEmit.h ResourceTracker.h SamplerEmit.h SetHashSuppressor.h SlotAllocator.{cpp,h} TextureEmit.h Tracker.h VertexInputEmit.h. There is no DrawEmit, no CommandEmit, no
QueryEmit, no FenceEmit, no PresentEmit — consistent with the 34 above.
5.3 What P5's exit gates actually require from that set
The P5 gates (ROADMAP.md:21) are
DirectGLES.Split.*(ClearThenReadPixels|Triangle) green under inproc, OpenRA trace split
SSIM ≥ 0.99, PersistentCoherentMapScenario green, both roles' peak RSS recorded,
persistent-map-push published, unmigrated field read = Fatal{UnmigratedPipeInput}.
Minimum new rows those gates force (my reading of the gate names, not stated anywhere — guess, but a well-supported one):
ClearThenReadPixels→Clear(57) +ReadPixels(58) +Present(67) +GetCaps(1).Triangle→ additionallyDrawVbo(59),SetShaderBuffers(38) if the shader has a UBO,Flush(66).- OpenRA trace → whatever that trace uses; almost certainly
Blit(56) and possiblyGenerateMipmap(54). PersistentCoherentMapScenario→MapPersistent/UnmapPersistent(already have appliers) plus the new client-side block-granularity push.
That is roughly 6–9 of the 34 for the reduced path. The remaining ~25 (queries, fences, XFB, indirect, compute, copy-region, texture writeback) are not on P5's critical path and the brief should say so explicitly, or the packages will try to build all 34.
6. Other facts the brief writer will want
MG_Remote/today is Protocol + Transport only — 19 files, noClient/, noServer/:Protocol/{mg_protocol_base.h, protocol.fbs, generated/protocol_generated.h}andTransport/{Ring,Doorbell,ShmSegment,ShmSegmentPosix,ShmSegmentWin32,FdPassing, InProcessTransport,Framing,ITransport,WireLog}. P5 creates bothClient/andServer/. Planned contents atARCHITECTURE.md:575(PipeEmitter EmitTables BackendObject_Remote CapsMirror ShadowArena PersistentMapTracker GpuWritePending Surface/{X11,Win32,Android,Headless}) and:388-393(PipeObjectTables,PipeApplier).InProcessTransportis explicitly not a test double and its header already warns what it does not cover: "What it does NOT exercise is serialization of the byte stream, so the framing codec is covered separately by FramingTest" (InProcessTransport.h:12-16). P5's charter says inproc must run the same G3 codec as spawn — so P5 has to change that property: inproc must stop queueing messages whole and start going through the ring + codec. Reading (b) is that only theSEG_CMD/SEG_STAGEring path goes through the codec whileITransportkeeps queueing control frames whole;ARCHITECTURE.md:441("热路径完全绕过它" — the hot path bypassesITransportentirely) supports (b). I read (b) as correct.- Build gating:
MOBILEGL_BUILD_DISAGGREGATEDisOFFby default (CMakeLists.txt:23); with it off, nothing underMG_Remote/compiles andnm --defined-only libMobileGL.so | grep -i MG_Remotemust be empty (CMakeLists.txt:18-20). Sources appended atCMakeLists.txt:503-518, define added at:572-573. It also hard-requires3rdparty/flatbuffers/include(CMakeLists.txt:453-464). MOBILEGL_TRANSPORTparsing lands inConfigLoader.cppat P5 (ARCHITECTURE.md:583), valuesmonolith | inproc | spawn | unix:<path> | pipe:<name>. TheInit.cpphook is one#if MOBILEGL_BUILD_DISAGGREGATEDbranch installingMG_Remote::BackendObject_RemotewhenMG_Config::Transport != Monolith(ARCHITECTURE.md:29, which notes the branch does not exist yet).- ctest wiring traps, already written down (
ARCHITECTURE.md:584): ctestENVIRONMENTis replace, not append;;must be escaped; the property overrides job env, somgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})is mandatory;add_trace_replay_testneeds theSPLITsuffix (otherwise it collides with the same case+backend name) and-DTRACE_TRANSPORT=forrun_trace_case.cmake. This matches MEMORY's ctest-ENVIRONMENT-overrides-job-env lesson from the CI integration lane. - The wire has no per-record sequence field — seq is the record ordinal
(
m_emitSeq++/m_applySeq++),ARCHITECTURE.md:124. gen_pipe.py --checkand--self-testare CI gates; the generated.incfiles are committed and CI diffs them (gen_pipe.py:25-33). Any P5 edit togen_pipe.pymust regenerate and commit, and must keep the self-test's negative controls tripping (gen_pipe.py:1196-1247).PipeCalls.def:26-29: record numbering never churns — a new call is appended, a retired call keeps its slot. The opcode is the 1-based position, so reordering is a protocol break. P5 must not reorder, andPipeCatalogueTest.cpp:418-422pins the last four opcodes (68/69/70/71) against exactly that.
7. Concrete asks for the P5 brief
- Add a generated per-opcode flags table to
gen_pipe.py(§2.7) before any emitter package starts, or six packages will each hard-code their own. - Fix or document the two unflagged blob carriers —
GetCaps/MGPCapsandCreateSamplerState/MGPSamplerDesc(§2.1).PipeCalls.defflag edits are cheap and do not move opcodes; the flag is what a generated encoder would key on. - Decide the chunking question explicitly (§1.4 item 5) — P5 refuses over-size records,
or P5 chunks
CreateShaderState. A per-recordcsob-blobhistogram settles it. - Name
EncodeProgramArtifacts/DecodeProgramArtifactsin the brief (§4.3) so no package writes a second program-archive codec. - Write down the
ReadPixelsdeviation — the payload comment says nothing in this catalogue blocks, P5's gate says blockingread_pixels(§2.4). - Scope the 34 rows (§5.3): name the ~6–9 the gates actually force, and say the other ~25 are out of scope for P5.
- Pin or accept the
MGPCapsABI hole (§4.3 item 4) before P6'sspawn.