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.
87 KiB
P4a scout — the MGPipe scaffolding and the P3a client conventions
Tree read: C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg, branch
feat/disaggregated @ 37da3c3a (P3a docs; landing hash fde5fda3). Every line number below was opened.
Repo-relative paths are under MobileGL/ unless the path starts with scripts/, .github/ or ~/w7/.
Read first: ~/w7/notes/p3a/INTEGRATOR-DECISIONS.md (ID-1..ID-24) and BRIEF-P3A.md §A (lines 108-207) and §C
(1012-1365). P4a's roadmap row is docs/Disaggregated/ROADMAP.md:20.
PART 1 — The MG_Pipe scaffolding P4a builds on
1.1 MG_Pipe/PipeCalls.def — the rows P4a implements
The file is 183 lines. MGP_CALL_LIST_DOCUMENTED_COUNT 71 at PipeCalls.def:75; the wire opcode is the 1-based
position in MGP_CALL_LIST (:29), so P4a may not reorder, insert into a group, or retire a row. A new call is
appended at the very end (the three appended rows at :160-166 are the precedent). Class tokens are unscoped
(MGPipe.h:29-37: kScreen kCtxCso kCtxState kCtxObject kCtxVerb kCtxQuery); flag tokens at MGPipe.h:39-55
(kNone=0, kNeedsAck=1<<0, kHasBlob=1<<1, kVarTail=1<<2, kHostSpan=1<<3, kReplySlot=1<<4, kOptional=1<<5).
Opcode = line position within MGP_CALL_LIST starting at PipeCalls.def:80. P4a's rows, with opcode:
| # | row | file:line | payload | class | flags |
|---|---|---|---|---|---|
| 2 | ResourceCreate |
:81 |
MGPResourceDesc |
kScreen |
kNone |
| 3 | ResourceRespecify |
:82 |
MGPResourceDesc |
kScreen |
kNeedsAck |
| 4 | ResourceDestroy |
:83 |
MGPHandleOnly |
kScreen |
kNone |
| 17 | CreateRenderState |
:98 |
MGPRenderStateDesc |
kCtxCso |
kHasBlob |
| 23 | CreateSamplerState |
:104 |
MGPSamplerDesc |
kCtxCso |
kNone |
| 24 | DeleteSamplerState |
:105 |
MGPHandleOnly |
kCtxCso |
kNone |
| 25 | CreateSamplerView |
:106 |
MGPSamplerView |
kCtxCso |
kNone |
| 26 | DeleteSamplerView |
:107 |
MGPHandleOnly |
kCtxCso |
kNone |
| 27 | CreateShaderState |
:108 |
MGPProgramDesc |
kCtxCso |
kHasBlob |
| 28 | BindShaderState |
:109 |
MGPHandleOnly |
kCtxCso |
kNone |
| 29 | DeleteShaderState |
:110 |
MGPHandleOnly |
kCtxCso |
kNone |
| 31 | SetFramebufferState |
:113 |
MGPFramebufferState |
kCtxState |
kNone |
| 35 | SetSamplerViews |
:117 |
MGPSamplerViews |
kCtxState |
kVarTail |
| 36 | BindSamplerStates |
:118 |
MGPSamplerStates |
kCtxState |
kVarTail |
| 37 | SetShaderImages |
:119 |
MGPShaderImages |
kCtxState |
kVarTail |
| 40 | SetGlobalConstants |
:122 |
MGPGlobalConstants |
kCtxState |
kHasBlob |
| 44 | SetDrawProgram |
:126 |
MGPHandleOnly |
kCtxState |
kNone |
| 45 | SetDispatchProgram |
:127 |
MGPHandleOnly |
kCtxState |
kNone |
| 47 | SetTextureParams |
:133 |
MGPTextureParams |
kCtxObject |
kNone |
| 48 | ResourceSubData |
:134 |
MGPSubData |
kCtxObject |
kHasBlob|kVarTail |
| 50 | ResourceSubDataComplete |
:136 |
MGPSubDataComplete |
kCtxObject |
kNone |
| 53 | ResourceCopyRegion |
:139 |
MGPCopyRegion |
kCtxObject |
kNone |
| 54 | GenerateMipmap |
:140 |
MGPMipPlan |
kCtxObject |
kNone |
| 55 | GetTextureImage |
:141 |
MGPReadbackInfo |
kCtxObject |
kReplySlot |
| 56 | Blit |
:143 |
MGPBlit |
kCtxVerb |
kNone |
| 57 | Clear |
:144 |
MGPClear |
kCtxVerb |
kNone |
| 58 | ReadPixels |
:145 |
MGPReadbackInfo |
kCtxVerb |
kReplySlot |
Notes that bind P4a:
- No
BindSamplerState/SetSamplerViewsingular calls exist;PipeCalls.def:53-57explains why: those two binds live only as the array forms underkCtxState, so the CSO group holds 13 rather than 15. CreateVertexElementsiskHasBlobandBindVertexElementsisMGPHandleOnly(:101-103) — the shapeCreateShaderState/BindShaderStatecopies.- The
kNeedsAcklegend (:18-24) says the flag means records of this call MAY require an ack, with the per-record predicateMGPipeResourceRespecifyNeedsAck(desc)deciding. Texture/renderbuffer respecify travels on the sameResourceRespecifyrow and inherits that predicate —desc.Immutable != 0(MGPipeTypes.h:681-683), which for a texture meansglTexStorage*. If P4a wants a different answer for textures, the predicate is where it changes, not the flag word. PipeCalls.def:169-183is the explicit-deletions footer:GetIntegeri_v/GetInteger64i_v,GetProgramiv,ShaderStorageBlockBinding(folded intoMGPProgramDesc's reflection archive),set_pixel_unpack_state, a compressed-format concept,pipe_transfer, and the stage dimension ofset_sampler_views(the texture unit space is merged, not per-stage).
1.2 MG_Pipe/MGPipeTypes.h — payloads and their asserts
Every payload carries MGP_ASSERT_POD(T, Size) (MGPipeTypes.h:44-46: trivially copyable + exact sizeof). The
macro is #undefed at :880. Sizes are asserted because wire records are memcpyd (:21-23).
P4a's payloads, verbatim from the header:
MGPResourceDesc:150-176,MGP_ASSERT_POD(..., 88)at:177;kMGPipeWholeBuffer = ~0ullat:178. Fields:Resource;Uint8 Target(Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer);Uint8 StorageKind(== TextureStorageType);Uint16 BindMask(12 bits, see §2.1);Uint32 InternalFormat("already resolved to an uncompressed fallback by the client");Width, Height, Depth;Uint16 ArrayLayers, Levels, Samples;Uint8 FixedSampleLocations, Immutable;Uint32 Usage(BufferUsage);Uint32 StorageFlags;Uint8 HasDefinedContent;Uint8 ImageBindableHint("client-side everImageBound; pre-emptive allocation");Uint32 GlNameForDiag(diagnostics only, never an identity/memo key/hash input —:171-174);MGPipeHandle ViewOf(storage owner for a texture view);MGPipeHandle BufferForTexBuffer;Uint64 BufOffset, BufSize. The texture half of this struct is already fully specified and unused — P3a wrote only the buffer fields (ResourceTracker.h:160-162pinskMGPipeResourceTargetBuffer = 0andkMGPipeResourceStorageKindBuffer = TextureStorageType::Buffer, and says the contract package "minted no enum for the first list"). P4a must mint theTargetenum, becauseBufferbeing 0 is currently justified only by "it is the leading member".MGPSurface:343-352,MGP_ASSERT_POD(..., 24):Res,Uint32 InternalFormat(inline "so the four cross-object masks fall out at push time with no lookup"),Uint8 Kind(Texture | Renderbuffer | None),Uint8 Layered,Uint16 Level,Uint32 Layer,Uint16 UploadTarget,Uint16 Pad0.MGPFramebufferState:355-377,MGP_ASSERT_POD(..., 304):MGPipeHandle Fbo(kMGPipeDefaultFramebufferfor the default FBO),MGPSurface Color[8],Depth,Stencil,MGPSurface ReadSurface— the RESOLVED read surface, not an index (:359-361, "structurally closes the read-buffer-shared-FBO defect class"; that defect is in memory asread-buffer shared FBO),Int8 DrawBuffers[8](attachment index,-1= NONE),Uint16 Width, Height, Layers, Samples,Uint8 FixedSampleLocations, IsDefault, Complete, Pad0,Uint32 Pad1,Uint64 ContentHashwith two jobs (:369-373): the server's render-pass memo key and the client's emission suppressor — "an unchanged hash means this record is not sent at all. The same pattern is mandatory for everykVarTailset_*below, or 26.2's redundantglBindSamplertraffic reappears as a variable-length record per batch."MGPSamplerDesc:283-286,MGP_ASSERT_POD(..., 32):Cso,MGPBlobRef Parameters.:279-282:SamplerParameterscrosses byte for byte includingborderColorForm, without which the backend cannot pick betweenglSamplerParameterIiv/fvor theVkBorderColorfamilies.SamplerParametersisMGPipeValueTypes.h:462,sizeof == 100asserted atMGPipeValueTypes.h:615. Note the row carries nokHasBlobflag inPipeCalls.def:104even though the payload holds anMGPBlobRef— an asymmetry P4a should either fix in the flag word or document (comparePipeApply.h:67-68, which saysresource_respecifydeliberately does not carrykHasBlobbecauseMGPResourceDeschas noMGPBlobRefmember; here the member exists).MGPSamplerView:293-303,MGP_ASSERT_POD(..., 36):Cso,Texture,Uint32 InternalFormat(aliasing format forglTextureView),Uint8 Target,Uint8 Pad0[3],Uint16 MinLevel, NumLevels, MinLayer, NumLayers,Uint16 Samples,Uint8 FixedSampleLocations,Uint8 Pad1.:288-292: only the view restrictions — anything aglTexParameterwrites goes onset_texture_params, because a texture that is only an FBO attachment, only an image binding, or only aglCopyImageSubDataendpoint has no sampler view to hang it on. (That sentence is the design source of the ROADMAP's mandatory new scenario.)MGPTextureParams:307-317,MGP_ASSERT_POD(..., 32):Res,Uint16 BaseLevel, MaxLevel,Uint8 Swizzle[4],Uint8 DepthStencilMode,Uint8 ForceResync(mirrorsm_forceTextureParamsResync— "the widened-channel carrier needs a swizzle override that the frontend params version does not move for"),Uint8 Pad0[2],Float MinLod, MaxLod, LodBias. Per texture OBJECT, independent of any view.MGPProgramDesc:323-335,MGP_ASSERT_POD(..., 192):Cso,Uint32 StageMask(== GetLinkedShaderStages()),Uint32 GlobalUboSize,Uint32 ReservedNumSamplesOffset,Uint8 SpirvStatus,Uint8 NativeFloat64,Uint8 PointSizeDemoted,Uint8 EnableSpirvValidation,MGPBlobRef Spirv[6](per stage),MGPBlobRef Reflection("the wholeLinkArtifacts+SpirvArtifactsarchive; P0.5 extracts those types out ofProgramObject.h").MGPBoundView:432-437,MGP_ASSERT_POD(..., 24):View,Texture,Uint32 Unit,Uint32 Pad0.:428-431: no stage dimension,TextureState::m_textureUnitsis one array ofMAX_TEXTURE_IMAGE_UNITS = 192.MGPSamplerViews:440-444,MGP_ASSERT_POD(..., 16):Uint32 Start, Count; Uint64 ContentHash. Var-tail =MGPBoundView[Count].MGPSamplerStates:447-451,MGP_ASSERT_POD(..., 16): same shape; var-tail isMGPipeHandle[Count]of sampler CSOs (:446).MGPImageView:453-462,MGP_ASSERT_POD(..., 24):Res,Uint32 Unit,Uint32 InternalFormat,Uint32 Layer,Uint16 Level,Uint8 Layered,Uint8 Access.MGPShaderImages:464-469,MGP_ASSERT_POD(..., 16):Uint32 Start, Count; Uint64 ContentHash.MGPGlobalConstants:507-513,MGP_ASSERT_POD(..., 40):ShaderCso,Uint32 Version,Uint32 Pad0,MGPBlobRef Blob.:506: default uniform block only (D6).MGPSubData:636-648,MGP_ASSERT_POD(..., 72):Res,Uint16 Target, Level,Uint8 SourceIsVerbatimLevelShadow(replaces the backend'suploadData == mipDatapointer comparison),MGPBox UnionBox,Uint32 RegionCount(MGPSubRegion[]in the tail),MGPBlobRef Blob.:611-613: carries the union box AND the region list so the SERVER picks the upload shape — "Mali prices texture upload by JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame". The buffer half convention (:615-618and the helpersMGPipeSetSubDataBufferRange:651-660,MGPipeSubDataBufferOffset:661-666,MGPipeSubDataBufferSize:667) is exactly what the texture path does not use: with a texture target,Level,RegionCountand the real box come back into play, andSubDataBoxFault's buffer-only refusals (PipeApply.cpp:510-531) must be branched onTarget.MGPSubRegion:607-614,MGP_ASSERT_POD(..., 40):Int32 X,Y,Z; Uint32 W,H,D; Uint64 SrcOffset; Uint32 SrcRowStride(0 = tightly packed),Uint32 SrcSliceStride.:602-605: source strides are carried, not inferred from a pointer comparison.MGPSubDataComplete:686-691,(..., 24);MGPCopyRegion:708-716,(..., 64);MGPBlit:718-727,(..., 56);MGPClear:729-739,(..., 48)(one discriminated record replacingglClear, fourglClearBuffer*, fourglClearNamedFramebuffer*);MGPMipPlan:741-745,(..., 16);MGPReadbackInfo:748-756,(..., 64)(Resnull forread_pixels: the bound read surface answers).MGPSurfaceInfo:872-878,(..., 24)— the reverse-channelOnSurfaceChangedpayload.
Two blanket rules P4a inherits:
- THE ONE BLOB RULE stated at
MGPipeTypes.h:270-285(onMGPVertexElements) and again at:619-631(onMGPSubData):Blob.Sizeis the record's own statement of its blob length; a non-zero size that disagrees with what the record's other fields describe isFatal{ProtocolCorruption}and the call is refused; a zero size means "this record does not declare its blob" (a monolith emission, bytes travelling beside the record through the entry point's companionconst void*) and is not a fault. The bytes read are always bounded by the record's other fields. The implementation of both halves lives atPipeApply.cpp:521-527andPipeApply.cpp:390-402. kMGPipeMaxVertexAttribs = 32at:378, pinned againstVertexArrayObject::MAX_VERTEX_ATTRIBSin the one TU that sees both —MG_Impl/Pipe/PipeFill.cpp:1035-1038. P4a needs the same shape for the unit/attachment/image bounds (there is nokMGPipeMaxTextureUnits,kMGPipeMaxColorAttachmentsorkMGPipeMaxImageUnitstoday;MGPFramebufferStatehard-codes 8 colours and 8 draw buffers).
1.3 MG_Pipe/PipeFields.def — the verify comparator's field lists
Purpose at PipeFields.def:9-19: one macro per payload listing the fields that carry MEANING; padding is
deliberately absent (a Pad<n> member is padding and is not listed) because MOBILEGL_PIPE_VERIFY must have zero
false positives. gen_pipe.py asserts — in both modes, hence in pipe-gates — that every list names exactly its
struct's direct data members; a member added without a row here fails the CI job.
P4a's existing rows (all already complete, none need adding unless a payload gains a member):
MGP_FIELDS_MGPResourceDesc :44-48; MGP_FIELDS_MGPSamplerDesc :74-75; MGP_FIELDS_MGPSamplerView :77-79;
MGP_FIELDS_MGPTextureParams :81-83; MGP_FIELDS_MGPProgramDesc :85-87; MGP_FIELDS_MGPSurface :89-90;
MGP_FIELDS_MGPFramebufferState :92-94; MGP_FIELDS_MGPBoundView :108-109; MGP_FIELDS_MGPSamplerViews
:111-112; MGP_FIELDS_MGPSamplerStates :114-115; MGP_FIELDS_MGPImageView :117-118;
MGP_FIELDS_MGPShaderImages :120-121; MGP_FIELDS_MGPGlobalConstants :132-133; MGP_FIELDS_MGPSubRegion
:156-157; MGP_FIELDS_MGPSubData :159-160; MGP_FIELDS_MGPSubDataComplete :162-163;
MGP_FIELDS_MGPCopyRegion :171-172; MGP_FIELDS_MGPBlit :174-176; MGP_FIELDS_MGPClear :178-180;
MGP_FIELDS_MGPMipPlan :182-183; MGP_FIELDS_MGPReadbackInfo :185-186; MGP_FIELDS_MGPSurfaceInfo :223-224.
MGP_VERIFY_PAYLOAD_LIST at :315-331 is what gen_pipe.py reads to know what to emit; every payload above is
already in it. The tail of the file (:226-311) holds the value structs the comparator must see into
(RenderStateParameters, PixelStoreParameters, PerBufferBlendState, StencilFaceState,
DynamicBackendParameters, MGHostSpan) plus P3a's two wire views MGP_FIELDS_MGPVertexAttribWire :306-308
and MGP_FIELDS_MGPVertexBindingPointWire :310-311. If P4a carries SamplerParameters as a typed value rather
than a blob, that is the pattern: a MGP_FIELDS_SamplerParameters row plus a P(SamplerParameters) entry.
1.4 MG_Pipe/Coverage.def — which accessor each call answers, and the emitted-list discipline
Header :9-25: gen_pipe.py joins scripts/data/backend_read_inventory.md (477 rows, 57 files) against
MGP_COVERAGE_ACCESSOR_LIST and MGP_COVERAGE_DELTA_LIST, writes generated/PipeCoverage.inc. Three pseudo-calls
stand for read points that never become a forward call: kClientResolved, kReverseChannel, kStructuralHandle.
Unmapped rows are counted in P0 and must be zero from P5 onward.
MGP_COVERAGE_ACCESSOR_LIST (:29-132) — the P4a-relevant rows already present:
| accessor | call | line |
|---|---|---|
GetActiveTextureUnit |
SetSamplerViews |
:30 |
GetFramebufferBindingSlot |
SetFramebufferState |
:84 |
GetImageTextureBinding |
SetShaderImages |
:85 |
GetMaxTouchedTextureUnit |
SetSamplerViews |
:88 |
GetProgramForDispatch |
SetDispatchProgram |
:99 |
GetProgramForDraw |
SetDrawProgram |
:100 |
GetProgramObject |
CreateShaderState |
:101 |
GetSamplingResolutionGeneration |
SetSamplerViews |
:109 |
GetTextureBindGeneration |
SetSamplerViews |
:112 |
GetTextureContextId |
SetSamplerViews |
:113 |
GetTextureObject |
SetSamplerViews |
:114 |
GetTextureUnitObject |
SetSamplerViews |
:115 |
No accessor row names SetTextureParams, CreateSamplerState, CreateSamplerView, BindSamplerStates or
SetGlobalConstants — those five calls have no backend read point in the inventory, they carry object
descriptors. That is the shape P4a inherits, not a hole to fill by inventing accessors: gen_pipe.py refuses a name
that is not an accessor.
MGP_COVERAGE_STICKY_LIST :144-151 — the seven F-class forwarded accessors, no per-verb generation.
Two are P4a's: GetProgramObject and GetTextureObject, both "keyed by GL name: an object lookup, not verb
state". :141-142: the verify lane's Fatal{UnmigratedPipeInput} is fixed by a FillPoints.def row, never by
a row here.
MGP_COVERAGE_DELTA_LIST :156-158 — read by gen_pipe.py only: handle-ify (wire handle) → kStructuralHandle, Buffer ops delta → ResourceRespecify.
MGP_COVERAGE_EMITTED_LIST :198-232 — the discipline P4a copies. The rules stated in the comment block
:160-197:
- every name must be an accessor in the accessor list and every call a real call in
PipeCalls.def; gen_pipe.pyturns it intokMGPipeFieldEmittedBy[]ingenerated/PipeFilled.inc— seegenerated/PipeFilled.inc:306-314, where theMGPipeFieldEmitterenum today is exactly{kNone, BindRenderState, BindVertexElements, CreateRenderState, SetDynamicState, SetPatchState, SetVertexAttribDefaults}. Adding a row mints a new enumerator, whichPipeFill.cpp'sSubsystemForEmitterswitch must then handle (that switch is exhaustive over the enum,PipeFill.cpp:957-976);- a field with no row keeps going through the fill loop, "which is what makes the
MOBILEGL_PIPE_PUSHbitmask a true per-subsystem A/B"; GetPixelStoreParametersis deliberately absent (:173-181): the field is both halves ofm_pixelStore[2]andset_pixel_pack_statecarries pack only; a row would be a half-truth;- P3a added exactly one row,
GetBoundVertexArray → BindVertexElements(:202), and:183-197explains that a row may land before the call that carries it exists, becausekMGPipeWiredSubsystemsis the guard. The final paragraph (:191-197) is written for the commit that wires it and is the model P4a should copy: the row is shape-only until the backend's twin resolution reads the applier's state instead of the object, andPipeFill.cpp'sEmittedCallSuppliesTheWholeFieldarm is where that is decided, deliberately rather than silently by the row's presence.
1.5 MG_Pipe/MGPipeHandles.h — the kinds
MGPipeKind at :24-40: None=0, Buffer=1, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context, KindCount.
P4a's kinds are Texture, Renderbuffer, Framebuffer, SamplerCso, SamplerViewCso, ShaderCso.
MGPipeHandle{Uint32 Slot; Uint32 Gen;}:54-61,sizeof == 8,alignof == 4asserted:63-65.Genincrements only when a slot is REUSED, never on a respecify (:44-48).kMGPipeNullHandle{0,0}:72;kMGPipeDefaultFramebuffer{0,1}:73— kindFramebufferslot 0 gen 1, which exists "so the fourpDefaultFramebufferInfo->defaultFBOidentity comparisons in DirectGLES retire into an ordinary handle compare" (:69-71). This is P4a's single most load-bearing reserved value.MGPipeHandleIsNull:75-77;kMGPipeFirstAllocatableSlot = 1:81.- The ShaderCso composite band
:83-97:kMGPipeShaderCsoSlotLimit = 1<<20,kMGPipeShaderCsoCompositeSlotBase = limit - (limit >> 4)(top 1/16),MGPipeIsCompositeShaderSlot(slot):92-94, and astatic_assertthat the band cannot swallow the ordinary slots:96-97. A composite is minted client-side out of the stage programs bound to a pipeline object and the server never learns it is a composite. The allocator honours the band atMG_Impl/Pipe/SlotAllocator.cpp:19-24(SlotIsAllocatablerefuses the band for kindShaderCso) and asserts exhaustion at:57-60.
1.6 MG_Pipe/PipeApply.{h,cpp} after P3a
MGPipeResourceOps (PipeApply.h:74-91)
struct MGPipeResourceOps {
void (*Create) (MGPipeHandle res, const MGPResourceDesc& desc);
void (*Respecify) (MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes);
void (*SubData) (MGPipeHandle res, const MGPSubData& record, const void* bytes);
void (*SubDataResident)(MGPipeHandle res, const MGPSubData& record, const void* bytes); // kOptional, may be null
void (*FlushRange) (MGPipeHandle res, const MGPFlushRange& record, const void* bytes);
void (*Readback) (MGPipeHandle res, const MGPReadback& record);
void (*Destroy) (MGPipeHandle res);
void* (*MapPersistent) (MGPipeHandle res, Uint64 size, const void* seedBytes);
void (*UnmapPersistent)(MGPipeHandle res);
};
void MGPipeSetResourceOps(const MGPipeResourceOps*); // :90
const MGPipeResourceOps* MGPipeGetResourceOps(); // :91
:52-56: the SECOND backend op table, beside BufferBackendOps; a null table means "this backend has not taken
the resource family over" and the frontend dispatches the old way — "which is what lets the client half land on its
own". :58-61: no frontend type appears here (gate G13 greps PipeApply.h for MG_State).
:63-68: the companion const void* is the client's own shadow base, zero-copy, and resource_respecify
deliberately does not carry kHasBlob because a kHasBlob record must own an MGPBlobRef and MGPResourceDesc
has none.
P4a's texture/renderbuffer resource_* calls travel on this same table. The signatures are already
handle+record+bytes and carry no buffer-specific type, so the table likely needs no new members for
create/respecify/subdata/destroy; what it lacks is any hook for GenerateMipmap, ResourceCopyRegion,
GetTextureImage, Blit, Clear, SetTextureParams and the sampler/program/framebuffer CSOs — those are new
tables or new members and that is a contract decision.
The applier's records and state (PipeApply.h:96-274, PipeApply.cpp)
- Bounds:
kMGPipeMaxResourceSlots = 1<<20andkMGPipeMaxVertexElementsSlots = 1<<16atPipeApply.h:112-113, with the argument at:97-111: a slot at or above these isFatal{ProtocolCorruption}— the same verdict as any other record that would make the server act outside its own storage — and never a resize. The tables grow only to the client's own dense high-water mark.RecordAtimplements it atPipeApply.cpp:404-425(returns null above the limit,resize(slot+1)otherwise). MGPipeRenderStateCsoRecord:42-46(Gen, Live, 396 pipeline bytes).MGPipeResourceRecord:117-132:Uint32 Gen,Bool Live,MGPResourceDesc Desc(the last create/respecify, verbatim — "the backend reads its Width / Usage / StorageFlags / HasDefinedContent instead of asking the frontend object"),Uint64 Serial(server-owned MGGen-class,++on every mutation; no MGPipe call may require the client to provide or know one),Bool HasLiveHostWrites— always false in P3a, written by nobody, pinned by a verify-only trip wire (PipeApply.cpp:533-547,PinNoLiveHostWrites).MGPipeVertexElementsRecord:141-152: Gen/Live/counts/two unpacked views/Uint64 ContentSerial.MGPipeApplierState:154-274. The two halves and their different lives are the single most important P3a ruling for P4a (:196-216): object records (Vector<MGPipeResourceRecord> Resources:217,Vector<MGPipeVertexElementsRecord> VertexElementsCsos:218) describe share-group objects and outlive a make-current; working state (BoundVertexElements:236,VertexBuffers/Start/Count:240-242,VertexFetchBaseInstance:250,IndexBuffer:262) is per-context and is cleared. P4a's framebuffer/sampler/ program/image bindings are working state; its texture/renderbuffer/sampler-CSO/program-CSO records are object records.- Refusal counters
:220-229:Uint64 RefusedResourceCalls,Uint64 RefusedVertexInputCalls. The rationale at:220-227and again atPipeApply.cpp:441-473:MOBILEGL_ASSERTcompiles out at INFO — which all three gate builds and every shipped build are — so "a no-op nobody can see is a dropped call nobody can see"; the counters are the observable in every build. The two resolvers that assert and count areResolveResourcePipeApply.cpp:478-486andResolveVertexElements:488-494, withkResourceRefusalNoteat:475-476("the record is not this applier's; the call is dropped, not applied"). P4a adds its own counter(s) in the same shape. The one legal refusal sequence is documented atPipeApply.cpp:452-462: teardown →MGPipeApplierReleaseObjectRecords()→~Object→ death notices naming records already dropped. - Serials that only advance, never zero:
VertexBuffersSerial:251-258,IndexBufferSerial:263-264, with the three-way argument spelled out atPipeApply.cpp:651-665(carry over → wrong immediately; restart at 0 → walks back through values already stamped into a twin that outlived the switch; advance → safe direction). BothMGPipeApplierReset(PipeApply.cpp:664-665) andMGPipeApplierReleaseObjectRecords(:677-678)++them. MapPersistentRoundtrips:266-273plus the long note atPipeApply.cpp:636-650: this member is per applier and zeroed at every make-current, whilePipeStats' process-widempr(emitted from the client) is what the gates read throughMG_IntegrationTest/Harness/PipeStatsWindow.h; nothing outsideMG_Testreads the member.
The three lifecycle functions
MGPipeApplier()PipeApply.h:277/PipeApply.cpp:596.MGPipeApplierReset()PipeApply.h:279-291, bodyPipeApply.cpp:601-666: a make-current, not a teardown; runs on every change of the currentGLContext; drops render-state CSOs, the residual mirror and the vertex-input working state; does NOT drop the object records (PipeApply.cpp:617-627: dropping them "made everyglBufferSubDataafter a context switch resolve to nothing and be dropped, with the only trace an assertion that compiles out at INFO"); does not clear the op table (:626-627).MGPipeApplierReleaseObjectRecords()PipeApply.h:293-300, bodyPipeApply.cpp:667-678: the served context's teardown; deliberately wired to NOTHING in the monolith — one applier behind every context, so calling it on one context's destruction would drop every other context's records. A record is cleared by its object's own death signal. The only caller in the tree is the unit fixtureApplierGuard(MG_Test/Pipe/ResourceEmitTest.cpp:146-157), whose comment says a test fixture "is the one caller in the tree that legitimately means 'this applier is going away'".
The never-destroyed singletons (ID-18, ID-21)
MGPipeApplierState& g_applier = *new MGPipeApplierState{};atPipeApply.cpp:384, with the reason at:380-383: destroy notices are raised from~BufferObject/~VertexArrayObject, released by exit handlers that run after this TU's globals are gone.MGPipeResourceTrackerInstance()—new, never deleted,MG_Impl/Pipe/ResourceTracker.h:554-560.MGPipeVertexInputEmitterInstance()—MG_Impl/Pipe/VertexInputEmit.h:428-440, with the concrete UAF it closes.MGPipeSetHashSuppressorInstance()—MG_Impl/Pipe/SetHashSuppressor.h:88-94;MGPipeCsoCacheInstance()—MG_Impl/Pipe/CsoCache.h:196-202;MGPipeSlots()—MG_Impl/Pipe/SlotAllocator.cpp(the rule's origin, cited by every other one). The rule stated in the comments is "every MGPipe process singleton", not only the ones on today's death paths — P4a's new singletons (a texture tracker, an FBO emitter) inherit it verbatim.- The other half of the closure is ID-21's
6515c8e6: leak-at-exit storage for the four staticPipeInputsholders. Proof recipe:GLIBC_TUNABLES=glibc.malloc.tcache_count=0on both lanes (ID-18, ID-20, ID-21).
The 23 apply entry points, with exact line numbers
PipeApply.h:309-329 declares the seven P2 ones; bodies PipeApply.cpp:680, 730, 749, 763, 772, 776, 834, 880.
P3a's nine resource entry points: declared PipeApply.h:347-380, bodies at PipeApply.cpp:951
(ResourceCreate), :984 (ResourceRespecify), :1010 (ResourceSubData), :1016 (BufferSubDataResident),
:1022 (ResourceFlushRange), :1051 (ResourceReadback), :1088 (ResourceDestroy), :1113 (MapPersistent),
:1142 (UnmapPersistent); the section header at :931-949 is worth copying verbatim as a template — "what the
applier owns here is IDENTITY, EXTENT AND ORDER — NOT CONTENT", and each body does three things in this order:
resolve the handle against the record, check the record against its own declared extent, then move the record and
hand the call to the backend.
P3a's five vertex-input entry points: declared PipeApply.h:386-404, bodies PipeApply.cpp:1173, 1263, 1279, 1303, 1349; the section header :1157-1171 says these five dispatch to nobody — the backend reads them at its own
draw-time sync out of the applier's working state, which is what the serials are for.
The derivation step MGPipeDeriveRenderStateFields / ...ForChunks is PipeApply.h:420 / :431.
Body-level idioms P4a should copy:
- A create starts the record over rather than editing it —
PipeApply.cpp:960-967: a recycled slot's record must not contribute one field. Generation is taken from the handle and nothing else survives.Serialstays 0 (a create is not a mutation) so a fresh backend twin starting at 0 agrees without either side publishing anything (:968-171). - Serial moves BEFORE the backend is told —
ApplyBufferWrite,PipeApply.cpp:578-582: the backend stamps its own synced serial from inside the hook, so a bump afterwards leaves the twin one mutation behind. - A readback moves no serial —
PipeApply.cpp:1062-1075, and the writeback precedes the epoch bump. - Destroy drops the record whole and keeps the generation —
:1092-1095; the client frees the slot afterwards, in that order. - A re-create on the same handle keeps the serial and counts up; a different identity starts over —
PipeApply.cpp:1200-1204; and it does not rebind (:1223-1226). - A dead handle leaves the previous binding untouched —
MGPipeApplyBindVertexElements,:1263-1278; the null handle is legal and means "nothing bound". - The var-tail window is the bound:
Start + Count > kMGPipeMaxVertexAttribsisFatal{ProtocolCorruption}, and entries outside the declared window are not cleared — "this record is 'the last set as received'" (PipeApply.cpp:1303-1324). Every P4akVarTailset inherits this.
1.7 MG_Pipe/MGPipeCallbacks.h — the reverse channel
Ten callbacks in struct MGPipeCallbacks (:27-51), count asserted at :55-57 (kMGPipeCallbackCount = 10,
sizeof == 10 * sizeof(void(*)())) — "an eleventh cannot be added without touching the transport's
reverse-channel record table". inline MGPipeCallbacks gMGPipeCallbacks{} at :60, null-initialised: a backend
that installs nothing sends nothing.
The six that are P4a's business: OnGlError :29; OnTextureWriteback(res, box, bytes) :33;
OnTexturePullRequest(res, target, firstLevel, levelCount, pullSerial) :37-38 — "the one new stall class in
this design (D-B6): the server recast a texture and needs its texels back. The client answers with zero or more
ResourceSubData records terminated by ResourceSubDataComplete carrying the same pullSerial";
OnMipLevelsGenerated(res, base, count) :41 — shape only, never bytes; OnSurfaceChanged(info) :44 —
"retires the layering inversion where the swapchain writes into MG_Impl's pDefaultFramebufferInfo";
OnCapsInvalidated() :45. :24-25: their order is a correctness requirement, not an optimisation.
The client's half of the channel is installed additively and never over an entry a backend already claimed —
MGPipeInstallClientResourceCallbacks() at ResourceTracker.h:623-630 (if (... == nullptr) ... = &Client...).
PART 2 — The P3a client conventions to copy
2.1 MG_Impl/Pipe/ResourceTracker.h (632 lines, header-only, all inside #if MOBILEGL_PIPE_PUSH)
The file header :12-40 states the shape: where it runs (the ONE exception to push-at-validate — the seven
BufferBackendOps hooks already dispatch at the GL call, so their pipe calls are emitted from the same
BufferObject dispatchers, not from MGPipeValidateForVerb); what lives here; header-only for the ownership
reason (the root CMakeLists.txt that would name a new .cpp belongs to the contract package); and no timer and
no per-call record copy on a hot path — the observables are written only by create/respecify, and the hot call is
observed through pure builders instead.
- The BindMask table
:60-149.enum MGPipeBindBit : Uint16:67-86givesMGPResourceDesc::BindMask's twelve bits,kMGPipeBindElementArray = 1<<11being the D-B7 switch.kMGPipeBindUnmapped = 0x10000u:92is a sentinel that is not a legal mask value;MGPipeBindMaskForBufferTarget:96-134has nodefault:arm on purpose, andMGPipeEveryBufferTargetIsMapped()+static_assert:136-149make adding aBufferTargeta build break. **P4a inheritskMGPipeBindSampler(1<<5),kMGPipeBindShaderImage(1<<6),kMGPipeBindRenderTarget(1<<7),kMGPipeBindDepthStencil(1<<8), which nothing sets today**, and:64-66` says explicitly: "They are spelled HERE rather than in MGPipeTypes.h because that header is the contract package's and the mask has, so far, exactly one producer: this file. The integrator moves them beside the field when a second producer appears (P4a's texture family)." That move is a P4a contract action. - Payload builders are pure
:164-235, so a unit case can assert field by field.MGPipeBuildResourceDesc(buffer, handle, bindMask, storageDefined):171-205, including the narrowing alarm at:180-194(a store ≥ 4 GiB cannot be declared, and truncating silently would make the applier's range gate name a corruption that is really a narrowing here).MGPipeBuildSubDataRecord:225-235fillsBlob.Seg = kMGHostSpanSegNoneandBlob.Size = size— "Leaving it 0 would be legal too; declaring it correctly is the stronger of the two." - The range splitter
:237-276:kMGPipeSubDataMaxRecordOffset = 0x7FFFFFFF,kMGPipeSubDataMaxRecordSize = 0xFFFFFFFF,MGPipeForEachSubDataRecordRange(offset, size, piece, maxChunk). Two properties are load-bearing: contiguous and ascending (:238-242) — splitting into overlapping or reordered pieces changes what the backend's queue-and-drain sees and the Mali WAR-stall fix depends on that queue being exactly the writes the application made. Every piece is proved encodable before the first one is emitted (:263-268): a half-emitted range is a partial content write landed as if it were whole.maxChunkis a parameter "exercised at a reachable value by the unit gate, so that lowering it is one argument rather than a new code path written under pressure". class MGPipeResourceTracker:282-550:Acquire(buffer):292-299— minting is NOT gated on a backend having registeredMGPipeResourceOps: the handle is client state andset_vertex_buffersnames it whether or not the resource family is on; only the CALLS are gated.Find:303-305never mints.Resolve(handle):313-320is the reverse-channel inverse: a raw pointer is exact (aWeakPtrwould be wrong at construction/destruction), and it double-checksMGPipeSlots().GenOfSlot(...) == handle.Gen.Retire(handle):325-329— the caller frees the slot afterwards, in that order, becauseFreeerases the lifetimeId→slot mapping.NotePublished/WasPublished:331-350— the create/destroy latch: the create is gated at its call site and the destroy inside the emitter, so the two ask the same question at two different moments; a buffer created while a table was registered and destroyed afterUnregisterBufferBackendOps()would take the second answer, free its slot, and leave the applier's recordLiveon a slot about to be re-handed-out. P4a needs one latch per kind it mints.BindMask:355-358(sticky, ORed, never cleared);NoteBoundAs(handle, target):369-374— closes the sampling window from the draw side;RefreshBindMask(ctx, buffer, handle):407-445— samples the frontend's live binding state at create/respecify, with the recorded deviation at:379-399(the brief pointed atBufferState.{h,cpp}but the.Bind()calls are inGL_Buffer.cpp, which C.5 assigns to nobody, so the mask is sampled instead of hooked) and the honest statement of the residual hole. TheBindEpochshutter:522-534and its stated blind spot:516-521(it does not see the 84×4 indexed binding points, sound only becauseBindBufferBase/Range_Statealso bind the generic slot). All of that is the template for a texture bind mask. Observables for unit cases:447-463:LastDesc(),CreateCount(),RespecifyCount(),DestroyCount(),MapPersistentCount(),NoteDesc(desc, isCreate),NoteDestroy(),NoteMapPersistent().ResetForTest()and its 20-line rule:465-498: "A buffer handle and the applier record it names are SHARE-GROUP OBJECT STATE… this tracker needs no re-publication path on a fresh context and must not have one: re-emittingresource_createfor a record the applier still holds would move itsSerialfor nothing." This is ID-12's B-C2 resolved, and P4a must state the same rule for its own trackers rather than leaving absence as policy.- The client's half of the reverse channel
:562-630:MGPipeClientOnBufferWriteback:570-589(monolith:SegiskMGHostSpanSegNoneandOffsetis the address of the backend's mapped bytes) andMGPipeClientOnGpuWritten:596-618, whose shape is a contract point: one range coveringkMGPipeWholeBuffer, deliberately not zero ranges, because zero will mean "fully narrowed" at P8/P9. The backend's producer side isMG_Backend/DirectGLES/Managers.cpp:2491-2521, which also records the other half of the contract (the client'sOnGpuWrittenmust set bothm_hasDefinedContentandm_gpuWritePending, or the next respecify orphans and drops shader-written contents).
2.2 MG_Impl/Pipe/VertexInputEmit.h (444 lines)
Header :11-38: these three emit at the validate point, from MGPipeValidateForVerb's step 3, in the fixed
order elements → buffers → index; the CSO is identity-addressed, not content-addressed (D-G1, a recorded
deviation), one handle per frontend VAO minted off its lifetime id, create_vertex_elements re-issued on the same
handle whenever the configuration moves. It also states what the unit gate reads and why the conversion is a pure
function per field (that is what makes G7 able to name a dropped field).
- Wire conversion
:59-116:MGPipeBuildVertexAttribWire(attrib, bindingIndex):82-107andMGPipeBuildVertexBindingPointWire(point):111-116. The comment:60-81enumerates, field by field, what the wire form carries and what it deliberately omits and why (Divisor travels inMGPVertexBuffer;LegacyStride/LegacyPointerare query answers and stay client-side;Bufferidentity travels inset_vertex_buffers; a survivingStride == 0is meaningful;IsLongis separate fromType == Float64). It asserts rather than assumes across the three narrowing casts:87-93. This is the exact form aMGPSamplerParametersWireor a texture-descriptor conversion should take. - ContentHash
:118-133:MGPipeVertexBufferSetContentHash(entries, start, count, baseInstance)—XXH64over the entries, thenMGPipeMixShutterofstart,countandbaseInstance. The comment:118-125states the hard requirement: the hash must cover every input the record carries, because the set is suppressed on an unchanged hash. P4a'sMGPFramebufferState,MGPSamplerViews,MGPSamplerStatesandMGPShaderImagesall carry aContentHash; the same rule applies to each. class MGPipeVertexInputEmitter:135-425:kAttribs/kBindingswith astatic_assertagainstkMGPipeMaxVertexAttribs:139-143.EmitVertexElements(ctx):158-193implements the three arms verbatim (:145-156): no VAO → bind the null handle; the bound VAO changed → (re)create if its configuration moved since this handle last published one, then bind; same VAO, configuration moved → create on the same handle and do not rebind. The latch is per handle, in a slot-indexed table, so ping-ponging between two VAOs re-binds but never re-creates.EmitVertexBuffers(ctx, baseInstance):195-248: one entry per attribute slot,Start = 0,Count= highest enabled attribute + 1; a client-memory array isRes == kMGPipeNullHandleand that is how the server learns the attribute is client-sourced; it callsNoteBoundAs(entry.Res, BufferTarget::Vertex)at every draw (:216-222, the bit that survives the DSA idiom); it goes throughMGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetVertexBuffers, hash):230-233and returns 0 bytes when suppressed.EmitIndexBuffer(ctx):250-273: emitted unconditionally (no suppressor slot),Offset/IndexSizeleft 0 because there is no draw at the validate point. Unit observables:275-290, none of which costs a copy — the emitter builds into the same arrays it hands the applier.RecordIsPublished(handle):292-311andNoteRecordDestroyed(handle):313-327— the record half of the latch, deliberately kept OUT ofReset()because object records are precisely whatMGPipeApplierResetdoes not clear. The comment:293-303records the trap: a slot can exist with no record behind it, because a backend that keys its twins on the handle mints the slot itself (BackendSlotTable::GetOrCreate → MGPipeSlots().Acquire) whether or not the subsystem ever asked the client to emit — which is exactly what aMOBILEGL_PIPE_PUSH=0x7flane runs.delete_*on such a handle is a refused call and the resolver asserts. Every P4a kind whose slot a backend twin table mints has this same trap.Reset():329-343— the per-context half only (Published,Gen,ConfigVersion, the bound handle); re-creating an unchanged configuration is a bounded over-fire, forgetting that the applier holds one would leak the record and its slot at the object's death.EmitCreate:359-406: all 32 of each, deliberately (the record declares both counts and the applier refuses one whose counts do not describe its blob; G6 is stated over all 32 slots); blob is attributes-then-binding-points, ascending and contiguous;Blob.Seg = kMGHostSpanSegNone,Blob.Offset = 0,Blob.Size = kAttribBytes + kBindingBytes; setslatch.RecordLive/RecordGen— the one producer of the record half (:400-403). Storage:408-424is fixed-sizeArrays, one blob buffer, one entries buffer; no allocation per draw.
2.3 MG_Impl/Pipe/PipeFill.cpp — the validate point and its discipline
- The resource emitters (the exception to push-at-validate)
:602-798:MGPipeResourceSubsystemEnabled():602-605=(Features.PipePush & kMGPipeSubsystemResources) != 0 && MGPipeGetResourceOps() != nullptr— both halves matter (PipeMutation.h:100-105).MGPipeResourceOpsHaveSubDataResident():607-610.MGPipeMintResourceHandle:612-624— unconditional in a push build.MGPipeEmitResourceCreate:622-637.MGPipeEmitResourceRespecify:638-683, including the self-healing create at:648-673(the create's gate and the consumer's gate disagree across a register/unregister boundary; a buffer born in that window latchedPublished = false, so every later respecify was refused and the twin drew through id 0 with no diagnostic anywhere).MGPipeEmitResourceSubData:684-704drives the splitter.MGPipeEmitMapPersistent:762-775is themprcounting site.MGPipeEmitResourceDestroyAndFree:777-799andMGPipeEmitVertexElementsDestroyAndFree:801-849— the two death paths, both returning "did the call go out"; the second has the three-position ordering argument (:836-848): applier record dropped first, the backend death notice raised second (it resolves the handle through the allocator, and a backend told after theFreecould no longer find its twin), the slot goes back last, and the double free is a proven no-op. Declarations are inMG_Pipe/PipeMutation.h:106-153, deliberately soMG_Statesees a declaration and never the client's tracker (PipeMutation.h:79-92; the closure gate's mutation-header probe enforces it). - Subsystem mapping and the pairing asserts
:955-1038:SubsystemForEmitter(MGPipeFieldEmitter):957-976(exhaustive switch over the generated enum); fivestatic_asserts pairing it againstMGPipeSubsystemForDirty:986-1002; P3a's three at:1004-1030, with the explicit lesson (:1016-1023): all three compare againstSubsystemForEmitter, not against the constant — pinning the dirty half to a constant asks a weaker question and would let an emitter row move onto another subsystem unnoticed. The escape hatch that existed at the contract commit (MGPipeSubsystemForDirty(...) == 0 ||) was removed once the dirty half was mapped (:1009-1015) — P4a will need the same hatch, and must remove it in the same commit that maps its bits. The vertex-attrib capacity pin is:1032-1038. kMGPipeWiredSubsystems:1071-1076= RenderState | PixelPack | PatchState | VertexAttribDefaults | Resources | VertexInput. The comment:1040-1070is the model: "a field whose emitter is not wired here keeps being pulled - so adding a row toCoverage.defcan never silently drop a field on the floor before the call that carries it exists"; each bit is added by the commit that gives its own emitters their bodies; and it states which bits retire a pull (neither of P3a's does). It also records the one-directional A/B trap (:1062-1069): bit 8 without bit 7 sends the server handles it cannot resolve and every one of those lands inRefusedResourceCalls; bit 7 without bit 8 is fine. Neither0x1ffnor0x7fis in that arm.EmittedCallSuppliesTheWholeField(field):1116-1124, returning false forGetPixelStoreParameters,GetCurrentVertexAttribute,GetBoundVertexArray. The 60-line argument:1078-1115is the one to reread when P4a decides its own rows — in particular theGetBoundVertexArrayparagraph (:1094-1115): the field's storage is aSharedPtr<VertexArrayObject>, the call carries an 8-byte handle, so the row is emitted-and-still-pulled; "what retires the pull is not a better applier - it is P8". Every P4a candidate (GetTextureObject,GetProgramObject,GetFramebufferBindingSlot,GetImageTextureBinding) has the same pointer-storage problem and will land in the same arm unless P4a also changes what the backend reads.AppliedWithoutDerivation(field)follows at:1126+.- The validate point
MGPipeValidateForVerb(verb):1505-1687. Step order: knob parse / verify arm / poison serial:1507-1521; set verb + identity:1522-1524; the no-context early return that still clears the pending base instance:1525-1535; step 2 the dirty walk:1541-1543; step 3 the emission gateconst auto wants = [&](MGPipeDirty bit){ subsystem != 0 && (pushMask & subsystem) && (dirty & bit) }:1550-1555— "Every gate below goes throughMGPipeSubsystemForDirty, the ONE map"; theFreshlyPrimedarm:1570-1585(CsoCache.Reset(),MGPipeApplierReset(),SetHashSuppressor.InvalidateAll(),VertexInputEmitter.Reset(),g_residualDue = true) with the P3a paragraph:1577-1584explaining that the emitter's reset is needed not because the applier dropped its records but because the emitter's bound-handle latch mirrorsBoundVertexElements, whichMGPipeApplierResetdoes clear, and that the resource tracker is deliberately NOT reset here; the four P2 emitters:1587-1598; P3a's vertex segment in the fixed order:1600-1616;tracker.ClearPendingBaseInstance():1620; step 4 the residual fill:1623-1655(with thesuppliedpredicate at:1643-1647= subsystem non-zero and wired and in the push mask andEmittedCallSuppliesTheWholeFieldand (applierDerives || AppliedWithoutDerivation)); step 4b the residual value block:1656-1678;PipeStats::RecordDrawPayloadBytes:1679-1686; the verifyEntryCompare:1683-1685. - The base-instance macro
MG_Impl/Pipe/PipeFill.h:114-123:MGP_FILL(Verb)andMGP_SET_BASE_INSTANCE(BaseInstance), the latter a macro because the three call sites compile in the pull build too, whereMGPipeSetPendingBaseInstanceis neither declared nor defined. Declaration + the rules atPipeFill.h:46-73. The three sites areMG_Impl/GLImpl/Drawing/GL_Drawing.cpp:662, 685, 715(ID-10's grant), one line each immediately above theMGP_FILL. Setter/getter/clear on the tracker:Tracker.h:453-455, member:477.
2.4 MG_Impl/Pipe/Tracker.h — the dirty bits, and which are P4a's
enum class MGPipeDirty : Uint32 at :57-84, the comment table is the roadmap:
| bit | name | line | comment says | subsystem today |
|---|---|---|---|---|
| 0 | NewRenderState |
:60 |
RenderState::m_version → set_dynamic_state |
RenderState |
| 1 | NewPipelineState |
:61 |
→ create/bind_render_state |
RenderState |
| 2 | NewPixelPack |
:62 |
→ set_pixel_pack_state |
PixelPack |
| 3 | NewPatchState |
:63 |
→ set_patch_state |
PatchState |
| 4 | NewVertexAttribDefaults |
:64 |
→ set_vertex_attrib_defaults |
VertexAttribDefaults |
| 5 | NewVertexElements |
:67 |
→ create/bind_vertex_elements |
VertexInput (P3a) |
| 6 | NewShader |
:67 |
the current program's link version | 0 — P4a |
| 7 | NewShaderBindings |
:68 |
image units, block bindings, uniform write set | 0 — P4a/P4b |
| 8 | NewGlobalConstants |
:69 |
the default-uniform-block image | 0 — P4a |
| 9 | NewVertexBuffers |
:75 |
→ set_vertex_buffers (P3a) |
VertexInput |
| 10 | NewIndexBuffer |
:76 |
→ set_index_buffer (P3a) |
VertexInput |
| 11 | NewFramebuffer |
:76 |
— | 0 — P4a |
| 12 | NewSamplerViews |
:77 |
— | 0 — P4a |
| 13 | NewSamplers |
:78 |
— | 0 — P4a |
| 14 | NewShaderImages |
:79 |
— | 0 — P4a |
| 15 | NewConstBuffers |
:80 |
— | 0 — P4b |
| 16 | NewShaderBuffers |
:81 |
— | 0 — P4b |
| 17 | NewSoTargets |
:82 |
— | 0 — P4b |
(The line-number collisions above are because two enumerators share a comment line; the enum order is exactly as
listed.) kMGPipeDirtyCount <= 32 asserted :87. The file header :22-26 says P2 emitted for bits 0..4, P3a adds
5, 9 and 10, "the rest are still computed, latched and counted so the per-bit fire rate is a measurement rather
than a plan, with their fields going through the residual fill until P3b/P4a/P4b". So P4a's fire rates already
exist as a measurement — FireCount(bit) / FireCount(bit, verbClass) / WalkCount() at :395-410.
Phase constants that are never edited in place (:93-104): kMGPipeDirtyEmittedAtP2 :96-99,
kMGPipeDirtyEmittedAtP3a :102-104 (= P2 | 5 | 9 | 10). P4a adds kMGPipeDirtyEmittedAtP4a beside them.
kMGPipeDirtyNames[] :106-127. MGPipeSubsystemForDirty(bit) :130-155 — P3a's three arms at :145-149, and
default: return 0 at :150-154 for "no call of its own until P3b/P4a/P4b".
The shutters P4a will narrow — all already computed in Update():
NewShader:249-262:MixShutter(program->GetLifetimeId(), program->GetLinkVersion()), deliberately notGetProgramForDraw(which joins a pending link; the tracker must not force a compile to answer "did the shader move").NewShaderBindings:253-258: a four-way mix ofGetImageUnitVersion,GetBackendStateVersion,GetBlockBindingVersion,GetUniformWriteSetVersion.NewGlobalConstants:259:MixShutter(lifetimeId, GetUBOContentVersion()).NewFramebuffer:303-306:MixShutter(GetAnyFramebufferAttachmentGeneration(), m_framebufferBind.Observe(GetFramebufferBindingSlot(Draw).GetVersion())).NewSamplerViews:307-308:MixShutter(textureContent, GetTextureBindGeneration()).NewSamplers:309-310:MixShutter(textureParams, GetSamplingResolutionGeneration()).NewShaderImages:311-312:MixShutter(MixShutter(textureContent, textureParams), programImages).
The composite-collision argument :157-171 (each mix is 64→64, ~2^-64 per pair, inputs an application cannot steer)
and the over-fire-is-free / under-fire-is-fatal rule :28-35 are what P4a's narrowing arguments must be written
against. MGPipeWidenedCounter :181-205 is how a wrapping Uint16 crosses the tracker boundary
(never widened in MG_State), with its one stated blind spot (:187-192: an advance of exactly 65536 between
walks reads as unchanged).
Reset() :365-393 — clears every latch, deliberately not m_pendingBaseInstance with the 12-line reason.
2.5 MG_Impl/Pipe/SetHashSuppressor.h — the seven slots
enum class MGPipeSuppressorSlot : Uint32 :44-53:
SetVertexBuffers = 0 (P3a — wired, and its hash includes BaseInstance), SetSamplerViews (P3b),
BindSamplerStates (P3b), SetShaderImages (P4b), SetShaderBuffers (P4b),
SetStreamOutputTargets (P4b), SetVertexAttribDefaults (P2 — wired), Count.
Note the phase labels in the enum disagree with ROADMAP.md:20, which puts set_sampler_views,
bind_sampler_states and set_shader_images in P4a. Three of P4a's four kVarTail sets therefore already have
slots (2, 3, 5 by index) and SetFramebufferState has none — it is not kVarTail, but it does carry a
ContentHash whose second job MGPipeTypes.h:369-373 states is exactly the client's emission suppressor. P4a must
either add a SetFramebufferState slot or suppress it another way, and update the phase labels it touches.
Mechanics: ShouldEmit(slot, contentHash) :61-67 — hash 0 is reserved for "never emitted" and a computed 0 is
remapped to 1 (:31-33: one collision in 2^64 costs an extra emission and never a missed one);
Invalidate(slot) :71; InvalidateAll() :73-75 (called from the FreshlyPrimed arm);
LastEmitted(slot) :79-81 exposed for the unit test that pins the reserved value. The file header :12-33 is the
whole rule, including that this is the carrier for the ~175 lines of debounce that move off the backends
(Espryt's UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged) in P3b and P4b.
2.6 MG_Pipe/DirtySurface.def — the rows P4a's mutators land in
The mechanism (:9-27, :150-155): scripts/gen_pipe_dirty_surface.py scans MG_Impl/GLImpl AND
MG_State/GLState (the second root is P3a's widening) for every pGLContext-> mutator call and every
MGP_NOTE_MUTATION site, and --check fails both directions. Answer vocabulary at :103-149:
NEW_*, kImmediate, kReverseChannel, kNoBackendRead, kExplicitDestroy, kUnpublishedDestroy,
kPulledEveryVerb, kPulledPartialShutter. The rule at :29-45: a row lists every publisher that fires on
EVERY path that mutates, because a shutter built from this file would otherwise under-fire; a redundant-write
guard does not make a publisher conditional, but a publisher reached on only some mutating paths must not be named.
The derivation is checked mechanically for every NEW_* answer (:46-60) with MGP_DIRTY_SURFACE_UNDECIDED_LIST
(:326-330, empty today) as the only escape, and a mark that outlives its reason is itself a red gate.
Rows P4a owns or will touch:
X(SetActiveTextureUnit, kImmediate):164.X(BumpTextureBindGeneration, NEW_SAMPLER_VIEWS):248.X(BumpSamplingResolutionGeneration, NEW_SAMPLERS):257— a P3a addition found only by the widened scan root (:249-256).X(NoteUnitTouched, kPulledPartialShutter|NEW_SAMPLER_VIEWS):270— the other P3a addition, and the reasonMGP_NOTE_MUTATIONbecame a recognised publish mechanism (:258-269): every texture and sampler bind routes through it, it moves two pushed fields with two different answers (GetTextureBindGenerationmoves only on thebindingChangedarm;GetMaxTouchedTextureUnithas no shutter at all and is copied at every verb).- The destroy family
:288-297/:296-306:MarkTextureObjectForDeletion,MarkFramebufferObjectForDeletion,MarkRenderbufferObjectForDeletion,MarkSamplerObjectForDeletion,MarkVertexArrayForDeletion,MarkBufferObjectForDeletionarekExplicitDestroy;MarkProgramForDeletion,MarkProgramPipelineForDeletion,MarkShaderForDeletionarekUnpublishedDestroy:301-305, i.e. a known hole: "programs, program pipelines and shaders have no per-object handle on the wire at all in P2". P4a's shader CSO is what closes that hole, and those three rows are what must change. X(SetNamedTransformFeedbackBinding, kPulledEveryVerb):284— with the note that narrowing is P3b's.
2.7 Subsystem bits — MG_Pipe/MGPipe.h:66-95
bit 0 kMGPipeSubsystemRenderState :72
bit 1 kMGPipeSubsystemPixelPack :73
bit 2 kMGPipeSubsystemPatchState :74
bit 3 kMGPipeSubsystemVertexAttribDefaults :75
bit 4 kMGPipeSubsystemResidualValues :76
bit 5 kMGPipeSubsystemEsprytSlots :77 (Track H, Espryt 0b)
bit 6 kMGPipeSubsystemMagmaVertexInput :78 (Track H, Magma subsystem 4)
bit 7 kMGPipeSubsystemResources :84 (P3a)
bit 8 kMGPipeSubsystemVertexInput :85 (P3a)
// bits 9..62 reserved for the later phases, allocated in ROADMAP order :86
bit 63 kMGPipeBehaviourNoCsoContentAddressing :90 (a BEHAVIOUR, not a subsystem)
kMGPipeSubsystemsMigratedAtP2 = 0x7f :94
kMGPipeSubsystemsMigratedAtP3a = 0x1ff :95
:66-70: bits are allocated in ROADMAP order and never reused — "an operator's recorded 0x7f has to keep
meaning what it meant"; a clear subsystem bit means "keep pulling", valid only while
MOBILEGL_PIPE_LEGACY_MEMOS compiles the pre-handle arm beside it (ARCHITECTURE.md:367 keeps that arm through
P3a/P4a). :80-83 justifies why P3a took two bits rather than one: separate A/Bs, "a buffer path that
regressed and a vertex path that regressed are different findings".
P4a therefore takes bits 9 and upward, in ROADMAP order, and adds
kMGPipeSubsystemsMigratedAtP4a beside :95 without editing 0x7f or 0x1ff. ROADMAP's P4a row names five
groupings (framebuffer state; sampler CSO; sampler view + texture params; the three unit-array sets; shader CSO +
draw/dispatch program + global constants + CompositeResolver; texture/renderbuffer resource_*), so the honest split
is likely 3-4 bits (framebuffer, texture-and-sampler, program, texture resources) — the P3a precedent argues for
splitting wherever two regressions would be different findings, and against splitting into an arm that is
non-functional on its own (PipeFill.cpp:1062-1069's bit-8-without-bit-7 trap: P4a's texture resource_* bit is
to its sampler-view bit what bit 7 is to bit 8, and the same paragraph has to be written for it).
2.8 Espryt's slot tables — the seventh is P3a's
MG_Backend/DirectGLES/SlotTables.h:138 defines BackendSlotTable<StateObject, BackendObject, kKind>; the
holder list (:420-436, s_firstHolder :541-543) is what makes DestroyByLifetimeId static-and-answered-by-every-
live-table. TwinRegistry instances, one per kind:
VertexElementsCso Managers.h:1181 / Managers.cpp:4673; Texture Managers.h:1521 / Managers.cpp:7001;
Framebuffer Managers.h:1612 / Managers.cpp:7754; ShaderCso Managers.h:2130 / Managers.cpp:8065;
SamplerCso Managers.h:2230 / Managers.cpp:10583; Renderbuffer Managers.h:2257 / Managers.cpp:10695.
The seventh, P3a's, is the buffer table at Managers.h:691
(BackendSlotTable<BufferObject, GLESBufferResource, MGPipeKind::Buffer>).
SamplerViewCso has no twin registry at all — it is the one P4a kind with no existing backend table.
The death dispatcher OnFrontendStateObjectDestroyed(kind, lifetimeId) is Managers.cpp:198-245: arms for
Texture :202, Framebuffer :205, Renderbuffer :208, SamplerCso :211, ShaderCso :214,
VertexElementsCso :217-231 (P3a's C-1 note: this is now the second, redundant path — the client speaks the
whole death), and the default: arm :232-243 recording that buffer death crosses as ResourceDestroy, so no
seventh raiser was added. The op table is installed/uninstalled at Managers.cpp:2529-2557
(RegisterBufferBackendOps / UnregisterBufferBackendOps), unconditionally in a push build, because the
subsystem bit is the frontend's dispatch predicate — "a build with bit 7 clear registers a table nobody calls and
the A/B stays a pure configuration question rather than a bring-up-order one" (:2532-2537).
PART 3 — The gates after P3a
3.1 scripts/p3a_untouched_regions.sh (467 lines) — the byte-identical set
Eleven functions in MobileGL/MG_Backend/DirectGLES/Managers.cpp (SOURCE_PATH at :85):
FUNCTIONS :87 = IsPoolable EnrollIntoPool AcquireFromPool TrimBufferPool ClearBufferPool ProcessDeferredBufferReleases CreateRingStorage RingAvailable RingAllocate FlushPendingRangesNow;
PINNED_FUNCTIONS :91 = FlushPendingRangesFrom with PINNED_BASELINE_REF=3e298c9a :92 and
PINNED_SHA_FlushPendingRangesFrom=37fc94ff… :93; EXPECTED_FUNCTION_COUNT=11 :95;
SELF_TEST_FUNCTIONS="ClearBufferPool FlushPendingRangesNow FlushPendingRangesFrom" :104.
The shape P4a copies (all from the header :1-84):
- Why a file diff cannot say it: the file is rewritten by design, so the claim is about bodies, extracted and
hashed on their own (
:5-11). - Each row is annotated with what the function is and why it matters (
:13-30). - The extraction is preprocessor-blind and mask-first (
:53-64): comments and string/char/raw-string literals are replaced by spaces of the same length, the definition is found as the one<name> (whose closing paren is followed (past qualifiers) by{, the body is brace-matched in the masked text and hashed from the ORIGINAL text — so a comment change inside one of these functions is a difference too, deliberately. - Exactly one definition must be found per name; zero or two is exit 2, never a silent pass (
:66-68). - Three usages (
:70-76):<ref-a> <ref-b>compares,<ref>prints the baseline list,--self-testproves the comparison can go red. stdout is always the sha list so the baseline capture is a plain redirect; everything else goes to stderr (:78-79). - Both arguments are git refs — an uncommitted edit is invisible by design (
:81-82). - Exit codes
:84-ish: 0 identical / 1 moved (naming the first in the fixed order) / 2 could not run. - ID-15's ruling is transcribed at
:32-42: two preprocessor arms, both hashed, the pull ladder againstBASELINEand the push ladder against a pinned sha, because that one was born in P3a and has no body at the base ref. P4a's own byte-identical set will have the same two shapes (functions that predate the phase → compared against the base ref; functions born in the phase → pinned sha captured at the commit where they were reviewed).
3.2 scripts/p3a_vertex_input_negative_control.sh (278 lines) — G7 for the vertex conversion
HEADER=MobileGL/MG_Impl/Pipe/VertexInputEmit.h :80, FIELD=IsBgra :81, TEST_NAME='VertexInputEmit\.' :82.
The design (:1-64): the break is chosen so the compiler cannot see it — the struct still has the member, still
24 bytes, its four static_asserts still hold, PipeFields.def still names it and the generated comparator still
compares it; what breaks is the value. IsBgra is picked because it has the least other coverage (Size stays
4 for GL_BGRA, so a dropped IsBgra does not even change the attribute's size). The patch is applied by regex,
not by an exact line, because the header belongs to another package (:25-29); if the field is not assigned at
all, that is exit 2, never a pass. Restore is not enough — the rebuild is part of the contract (:38-49):
every exit goes through repair() (restore, rebuild, re-run), which also runs from the EXIT trap, and a failed
repair downgrades the verdict to 2. Logs live in <build-dir>/p3a-g7-logs/, inside the build tree so nothing is
left in the repository (:51-53). Exit codes :55-62: 0 tripped and named the field; 1 the control did not
answer (green with the field dropped, or red without naming it — both findings about the test); 2 could not run.
Not a CI lane — it rebuilds the library twice (:31-32).
scripts/g7_negative_control.sh is P2's sibling for the pipeline/dynamic split: it demotes ColorMasks out of
pipeline chunk P1 by inserting two boundaries, expects RenderStateSpansTest to go red naming SetColorMask,
relaxes the four measurement pins so the patch stays a semantic break rather than a build break (:33-36), and has a
--verify-patch-only mode (:41-45) that checks the mechanism without claiming the control passed.
3.3 The itest lanes and their MOBILEGL_PIPE_PUSH pins
MobileGL/MG_IntegrationTest/CMakeLists.txt (1389 lines). The scenarios are at
Scenarios/{HandleRecycle, CsoContentAddressing, ResourceSubsystemControl}Scenario.cpp :133-135.
- The Handles-arm knobs
:948-955:MGL_ITEST_HANDLES_ARM_KNOBS = "MOBILEGL_PIPE_LEGACY_MEMOS=0" "MOBILEGL_PIPE_PUSH=0x1ff",MGL_ITEST_ABA_ARM_KNOBS = "MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1", empty in a pull build. The mask is the PHASE DEFAULT, not a hand-picked bit (:936-947): it was0x7fand is now0x1ff, because "pinning it at0x7fafter P3a would leave the Handles arm asserting the P2 shape while the buffer and vertex-input handles it is supposed to be about stayed switched off - a lane that still passes and no longer measures the key that ships". P4a must raise all of these to its own phase constant — this is closed contract-review item 11 (:1120-1128) and it is stated for all three lane families. - Six
HandleRecyclelanes:975-1030:DirectGLES.HandleRecycle.Handles.,DirectVulkan.HandleRecycle.Handles.,DirectGLES.HandleRecycle.Legacy.(MOBILEGL_PIPE_PUSH=0),DirectVulkan.HandleRecycle.Legacy.,DirectVulkan.HandleRecycle.AbaControl.(PUSH=0+ the ABA knob),DirectVulkan.HandleRecycle.AbaControlHandles.(the handles knobs + the ABA knob). The last one exists because "the guard P2 SHIPS is the generation: a control that only defeated the retired guards would be green forever" (HandleRecycleScenario.cpp:87-93). - Four
CsoContentAddressinglanes:1067-1105and their env:1130-1146:0x1fffor On,0x80000000000001fffor Off (bit 63 + the phase default) — the mask moves with the phase, the control is bit 63, and the two must not be confused (:1112-1128). - Two
ResourceSubsystemControllanes:1147-1175: On =0x1ff, Off =0x7f— "the two phase constants, not hand-picked bits". DirectGLES only, because Magma registers noMGPipeResourceOps(:1121-1124). - Two
MapPersistentRoundtripscounting lanes:1177-1210, each selecting one case byTEST_FILTERwith a privateMOBILEGL_LOG_FILE_PATH, plusMOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=1. The private-log rule is stated three times (:1035-1046,:1126-1140,Harness/PipeStatsWindow.h:19-25): the library opens the logfopen(path,"w"), so every process in a lane truncates it, and two readers in one lane race underctest -jwith a failure that looks exactly like "the counter was never emitted". - Two
LargeArenaAdoptionA/B lanes:1212-1243:ResourceSubsystemOn.(0x1ff) andResourceSubsystemOff.(0x7f) over the whole scenario. - Everything is registered in EVERY build, including the pull build, so
ctest -L integration-gpustays name-for-name identical between pull and push (G2); in a pull buildMGITEST_PIPE_PUSH_BUILDis absent and the cases SKIP saying so (:362-378,:408-506,:1141-1146). The CMake side greps the backend sources for the subsystem constant and the knob name and passes the answer in asMGITEST_HANDLE_REKEY_<backend>/MGITEST_HANDLE_ABA_IMPLEMENTED, withCMAKE_CONFIGURE_DEPENDSso the answer cannot go stale (HandleRecycleScenario.cpp:98-103). - The verify block
:1246-1389:MGL_ITEST_VERIFY_TIMEOUT 900; the ambientDirectGLES.Verify./DirectVulkan.Verify.re-registrations labelled"integration-gpu;integration-verify"; the arming lane with its own log andMGITEST_PIPE_ARMING_LANE=1; negative control A (MOBILEGL_PIPE_VERIFY_CORRUPT= GetRenderStateParameterswithMOBILEGL_PIPE_VERIFY_FATAL=0so the process survives and can read its own report back) and negative control B (MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit, the scenario forks so theabort()is a datum inwaitpid()).
3.4 HandleRecycleScenario — the arms, and which kinds already have ABA controls
Scenarios/HandleRecycleScenario.cpp. Three arms enum class Arm {Handles, Legacy, AbaControl} :141-145,
selected by the harness marker MGITEST_HANDLE_ARM (:146-152; the library never reads it). The arm
descriptions are at :74-93, the skip policy at :95-113 ("never a silently-deleted registration and never a green
that means 'the thing I test does not exist yet'").
Seven cases:
| line | case | kind |
|---|---|---|
:521 |
TheReproducerRecyclesEveryName |
— |
:568 |
AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInput |
VertexElementsCso |
:677 |
(continuation case, VAO/vertex-input) | VertexElementsCso |
:768 |
ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents |
Buffer (P3a) |
:869 |
ATextureAtARecycledAddressDoesNotInheritItsPredecessorsTwin |
Texture |
:920 |
AFramebufferAtARecycledAddressDoesNotInheritItsPredecessorsTwin |
Framebuffer |
:1015 |
DestroyedVertexArraysReturnTheirVertexElementsSlots (the leak test) |
VertexElementsCso |
The texture and framebuffer cases already exist but are NOT ABA controls: :865-868 says in as many words "The
AbaControl knob does not steer this path, so this case expects the correct pixels in EVERY arm — stated explicitly
rather than by omission", and the assertion is
ExpectPixelsFor(m_arm, /*armExpectsCorruption=*/false, ...) :903-904. The framebuffer case's readback is
deliberately not through the framebuffer under test (:913-918): it uses glGetTexImage on the replacement's own
attachment, so "the clear went somewhere else" is visible. P4a's kinds (Texture, Framebuffer, Renderbuffer,
SamplerCso, SamplerViewCso, ShaderCso) need armExpectsCorruption=true controls of their own, i.e. the
MOBILEGL_PIPE_HANDLE_ABA_CONTROL knob has to defeat the identity half of the memo keys P4a introduces
(the Magma side of that knob is MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h's
MagmaPipeAbaControlDefeatsIdentity, per ID-1). Renderbuffer, SamplerCso, SamplerViewCso and ShaderCso have no
case at all today.
3.5 The leak test and the PipeSlotPeek harness
Harness/PipeSlotPeek.h (44 lines) + .cpp. enum class PipeSlotKind { Buffer, VertexElementsCso } :29-32 —
"Mirrors MG_Pipe::MGPipeKind for exactly the kinds a scenario has a reason to count, so that the enum does not
travel through this header and the GL headers together". P4a extends this enum for each kind it mints.
bool PeekPipeSlotLiveCount(kind, unsigned*) / PeekPipeSlotHighWater(kind, unsigned*) :41-42, both returning
false, touching nothing, where the allocator is out of reach — a pull build has none, and on Android the module
links the shipping libMobileGL.so built -fvisibility=hidden. :37-39: a caller that gets false must SKIP
rather than pass — "could not look" is not "did not leak". It is its own translation unit because the scenario
sources include the GL headers with prototypes (:19-21).
The leak case DestroyedVertexArraysReturnTheirVertexElementsSlots HandleRecycleScenario.cpp:1015-1110:
skips unless m_arm == Arm::Handles (:1019-1025: the client mints the slot only when the subsystem is on);
skips if the peek is out of reach (:1027-1032); one shared VBO so only the VAO kind's slots churn;
one round = create → DRAW (which is what mints the slot and publishes the record) → unbind → delete;
two warm-up rounds before the baseline is taken (:1076-1085, so what is measured is growth with the churn and
not the one-off cost of drawing at all, the default VAO's slot above all); kChurn = 48; three assertions
(:1098-1110): liveAfter == liveBefore, highWaterAfter == highWaterBefore, peakLive - liveBefore <= 1.
The failure message names the cost (a SlotState, a lifetime-id map node, the applier's ~1.3 KB record, and past
kMGPipeMaxVertexElementsSlots a permanent Fatal{ProtocolCorruption}) and the backend. :1005-1013 records why
the four ABA cases ran green over the leak: the observable is the allocator, not pixels — the leak produces
correct pictures the whole way to the fatal.
3.6 The verify lane, retrace_gate.py and wsl_p3a_gate.sh
- Arming line (ID-4 errata in
BRIEF-P3A.md:150-153):MGPipe: verify armed - 63 fields, 69 verbs, fatal=1; per-case retrace logs carryMGPipe verify: <case> <backend> armed, zero divergences, zero unmigrated reads. The field count is pinned ingenerated/PipeFilled.inc:96(kMGPipeInputFieldCount == 63). - ctest name extraction, mandatory (ID-4):
grep -E "^[[:space:]]*Test[[:space:]]+#[0-9]+:" | sed -E "s/^ *Test +#[0-9]+: //" | LC_ALL=C sort— the naive^\s+Test #drops every id under 1000. ~/w7/retrace_gate.py:--tree --lib --out [-j] [--only REGEX](:9, args at:34-39).--onlyis a REGEX, applied asre.search(a.only, k)at:51— ID-15 records that the gate script once passed a comma list and matched 0 cases while reporting success. There is no--ssimand no--backend; the threshold is the per-case one intrace_cases.json.~/w7/notes/tools/wsl_p3a_gate.sh(70 lines),usage: wsl_p3a_gate.sh [quick],BASE=44c2b5cf:7. Three builds first and fails fast (:20-22: "results from a stale binary must never look like a verdict").FAMILY:12is the buffer/VAO regex (LargeArenaAdoption|StorageBufferRegrow|VertexAttribBinding|MultiDraw|PrimitiveRestart|CrossFrameBuffer| ResidentIndex|BufferTexture|AtomicCounter|XfbCaptureBufferReuse|PackedWordReadback|DoublePrecision| VertexArrayEnableDisable|DrawParameters) — P4a needs its own FBO/texture/program family regex.NAMED:13is the G3b regex, with the comment# retrace_gate.py --only is a regex. Part 1 (purity):24-32: include closure, thepGLContextgrep, theMG_State-in-PipeApply.hgrep,symbol_report.py --threshold 0, the Resized table,p3a_untouched_regions.sh $BASE HEAD, theRenderStateImplsha,HandleRecycleunder verify. Part 5:34-38:gen_pipe.py --check/--self-test,gen_pipe_dirty_surface.py --check/--self-test,ctest -R 'RenderStateSpans\.|Residual|VertexInputEmit\.|ResourceEmit\.', the G5 self-test. Part 3:40-52: G2 name diff, G14 removed/added, unit ×3 builds, then fiveintegration-gpuarms — default,MOBILEGL_PIPE_PUSH=0,MOBILEGL_PIPE_PUSH=0x7f,MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1, and the family subset — plusg7_negative_control.sh,p3a_vertex_input_negative_control.sh,CsoContentAddressing|ResourceSubsystemControl, and the verify controls (PoisonOmitted\.|VerifyCorrupted\.|HandleRecycle). Part 2:54-68:integration-verify, the Fatal-line census, then the three retrace sweeps (verify / push / named) with the armed-count and Fatal-log checks, skipped byquick. PrintsP3A_GATE_DONE.- Tree/integration helpers are phase-parameterised (ID-3):
wsl_tree.sh p3a <slug> <base> [verify]andwsl_integrate.sh p3a <slug>….
3.7 CI (.github/workflows/test.yml)
Triggers :3-21: pushes to dev, the two backend branches, and — # TEMPORARY, remove before merging the MGPipe work into dev :9-12 — feat/disaggregated, so a phase's landing is not gated on someone dispatching by hand;
plus workflow_dispatch with a baseline_sha input defaulting to 087685d1 :13-21 (the SYMBOL baseline, not
the phase base ref).
Jobs :24-1554: build-linux, test, integration, build-linux-verify, integration-verify, flatc-check,
include-graph-check, benchmark, build-retrace, trace-cases, trace-fixtures, retrace, retrace-summary,
retrace-verify, monolith-symbol-report, remove-artifact-clutter, pipe-gates :1554.
pipe-gates (:1554-1687), "deliberately independent of build-linux: these are source-level gates, they take
seconds, and a broken build must not hide a drifted interface", env: BASELINE: "44c2b5cf" with the note
distinguishing it from baseline_sha :1560-1565, and fetch-depth: 0 because the G5 gate reads Managers.cpp at
BASELINE with git show :1567-1572. Steps:
Regenerate the MGPipe interface (G1-G7)—python3 scripts/gen_pipe.pythengit diff --exit-code -- MobileGL/MG_Pipe/generated:1579-1583.The MGPipe generators' checks can still fail—gen_pipe.py --self-test:1585-1591.The symbol report's buckets and gates can still fail—symbol_report.py --self-test:1593-1595.No stdio instrumentation in MG_Backend or MG_State:1597-1612— one grep alternation coveringfprintf(stderr|stdout),printf(,puts(,std::cout|cerr, with no exceptions.MGPipe dirty-surface mapping is complete (G9)—gen_pipe_dirty_surface.py --checkand--self-test:1614-1636.The buffer pool, the deferred-release drain and the rings did not move (G5):1638-1665, guardedif: github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch'— "the question is 'did these eleven move since P3a started'… it belongs with the TEMPORARY trigger lines at the top of this file and retires with them".The untouched-region gate can still fail (G5)— the--self-test, same guard:1667-1669.Documentation citation lint— warning only (|| true) while the documents settle:1671-1687.
ID-16 / ID-22 record the two reds that are not code signals: GitHub artifact-download digest-mismatch on
retrace matrix entries, and an HTTP 504 from gh api --method DELETE in "remove artifact clutter" after every test
passed.
PART 4 — The measurement infrastructure as of P3a
All under ~/w7/notes/tools/ (integrator-owned, ID-3).
wsl_build_trace_apks.sh(29 lines).usage: wsl_build_trace_apks.sh <phase> [tree]; builds the trace-flavour APK (arm64-v8a, release, debuggable, INFO) twice — pull, then push via-Pmobilegl.pipePush=ON— and debug-signs both, because a WSL build has noSIGNING_*keys and the release APK comes out unsigned (adb installfailsINSTALL_PARSE_FAILED_NO_CERTIFICATES). It creates~/.android/debug.keystoreif absent, wipesandroid-plugin/app/.cxxbetween arms, and prints the lib size plus theMGPipeandmap-persistent-roundtripsstring counts for each APK — ID-17's guard, so an -O0 APK can never pass unnoticed again (Release.text≈ 9.4 MB; the P2-era A/B APK's 21.8 MB.textwas a Debug-flavour build). Output:~/w7/notes/<phase>/apk/trace-{pull,push}.apk.p3a_ab.sh(53 lines) — the paired device A/B, run from Windows Git Bash withadb.exeon PATH.usage: p3a_ab.sh <serial> [case …]; default cases:improved-transparency-minecraft-26.3,minecraft-1.21.4-rd12-odinlite-in-world,minecraft-1.21.4-fabric-sodium-in-world,minecraft-1.21.1-neoforge-create-instancing-in-world. Matrix{pull,push} × {nofinish,finish} × cases × {DirectGLES,DirectVulkan}. Protocol: reboot-clean once, wait forsys.boot_completedand for root, wake, pin verified and retried up to 3 times (pin_device.sh pinthencheck, exit 43 on failure), then a device-lock hold per run (mkdirlock with a 900 s timeout → exit 42) andpin-before.txt/pin-after.txtaround each run. The runner istools/trace_replay/run_android_retrace_local.py --case … --backend … --benchmark [--benchmark-no-finish] --benchmark-repeats 3 --benchmark-tail-frames 200 --env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120. Outputs go to a Windows-local scratchpad dir (Git Bash cannot create directories on//wsl.localhost) and are copied back bycopy_ab.sh. Note at the top: do not exportMSYS_NO_PATHCONVglobally here.p3a_ab_7f.sh— the same script with four differences (verified by diff): the output root (p3a-ab7f), the loop restricted toarm=push×mode=nofinish, the directory suffixpush7f, and--env MOBILEGL_PIPE_PUSH=0x7f. It is ID-17's supplementary arm: the same push APK run with P2's subsystems only = the device-side T2.ab_reduce3.py(84 lines) —usage: ab_reduce3.py <main-root> <7f-root> <out.md>. Reads the runner's own "best of N" line (lowest mean wall frame time repeat) for CPU p50/p99 over the trailing window (runner_best:14-24), the pin verdicts frompin-before/after.txt(pin:26-37), and the summedmpr=out of everyMGPipe stats: frames=line inmobilegl.log(mpr_total:39-49). Emits a 13-column table: pull p50 | P2-only (0x7f) p50 | P3a (0x1ff) p50 | Δ P2 | Δ P3a total | pull p99 | P3a p99 | pins pull/7f/P3a | mpr 7f/P3a, with the footnote "the P3a-only share is the difference of the two".wsl_p3a_bench.sh(62 lines) — desktop DriverBench, run on an idle box (no gate, no builds).REPEATS=5,DRIVERBENCH_FRAMES=240, casesmc_vanilla_draw mc_state_toggle mc_pass_switch, lavapipe/llvmpipe ICDs,EGL_PLATFORM=surfaceless. Eleven arms:native, and for each ofespryt/magma:-pull,-push,-push7f(MOBILEGL_PIPE_PUSH=0x7f= T2),-push0(=0),-nocso(=0x80000000000001ff). Writesdriverbench.csv(every repeat) anddriverbench.md(medians + T1 = push(0x1ff) − pull, T2 = push(0x7f) − pull, T1 − T2 = what the phase added).- The recorded numbers (ID-19, ID-24), which are P4a's comparison baseline:
desktop
mc_vanilla_drawns/draw — Espryt pull 5115 / push(0x1ff) 6162 / push(0x7f) 5463 / push(0) 5693; Magma pull 17099 / 17709 / 17287 / 17739. T1 = +1048 (+20.5%) / +611 (+3.6%); T2 = +349 / +189; P3a itself = +699 / +422 ns/draw — the largest boundary cost so far. Device (Release APKs from3e298c9a): P2's boundary = +6-12% p50 on both devices and backends; P3a adds ~0 on 26.3 and sodium but +17-19 points on rd12 (Espryt +30%, Magma +27% total); MC 26.3 p99 on Adreno 25.46 → 26.32 ms (+3.4%).mpron device: 26.3 = 8 per run, sodium 1. - Thermal / device traps (ID-23 and the
fcl-device-testingmemory): rd12 on Magma on the Xiaomi aborts on every arm including pull (a scudo map error inside acallocfrom libMobileGL seconds after start, with 6 GB free) — pre-existing, excluded from the table, task chip open. Side effect: the GPU pwrlevel pin resets after the crash (pin_devicethen reports "range 2..5"), so every row taken after rd12 on that device carries a DRIFT verdict on both arms (still paired). Also excluded:create-instancing(a 2-frame fixture, not a benchmark) and sub-ms sodium rows (noise). The protocol memory adds: CPU big 1.96 / little 1.55 GHz fixed, GPU pinned, a 40 °C entry threshold, downclock afterwards, and the harness'spidofsingle-sample kill is on the optimisation list. - Artefact locations (ID-24):
~/w7/notes/p3a/ab(60benchmark.json),ab7f(15),ab3-final.md; bench~/w7/notes/p3a/bench/driverbench.md; APKs~/w7/notes/p3a/apk/.
What P4a has to decide that P3a did not
Collected, each with the file that forces the question:
- The
MGPResourceDesc::Targetenum does not exist.ResourceTracker.h:154-162says the contract package minted none and thatBufferis 0 only because it is the leading member of a comment. Every texture target,RenderbufferandTexBufferneed real values, inMG_Pipe, before a second producer writes the field. MGPipeBindBitmust move out ofResourceTracker.h.ResourceTracker.h:64-66names P4a's texture family as exactly the second producer that triggers the move, and four of the twelve bits (Sampler,ShaderImage,RenderTarget,DepthStencil) have no writer today.kMGPipeMaxResourceSlotsis one bound for all resource kinds (PipeApply.h:105-113argues the two existing bounds differ because the two records differ). A texture record carrying an 88-byte descriptor plus per-level state is a third size class.MGPSubData's buffer-only gate (PipeApply.cpp:510-531) refusesLevel != 0andRegionCount != 0unconditionally; the texture path needs the branch onTargetplus a real box/level/region validator.SamplerViewCsois the one P4a kind with noTwinRegistry(Managers.hhas six,:1181/:1521/:1612/:2130/ :2230/:2257, plus the buffer table:691), and therefore no backend death arm inOnFrontendStateObjectDestroyed(Managers.cpp:198-245). Its lifetime is client-only, likeVertexElementsCsoafter C-1.kUnpublishedDestroyfor programs/pipelines/shaders (DirtySurface.def:301-305) is exactly what P4a's shader CSO closes; three rows change and the gate checks both directions.SetFramebufferStatehas noMGPipeSuppressorSlotalthough itsContentHash's stated second job is client suppression (MGPipeTypes.h:369-373); and the three slots P4a will wire are labelledP3b/P4binSetHashSuppressor.h:46-50, which disagrees withROADMAP.md:20.- The
EmittedCallSuppliesTheWholeFieldanswer for every P4a accessor is almost certainly "no" for the same pointer-storage reason asGetBoundVertexArray(PipeFill.cpp:1094-1115); that has to be decided in that function, deliberately, and not inherited from aCoverage.defrow's presence. MGPSamplerDesccarries anMGPBlobRefbut itsPipeCalls.def:104row iskNone, unlike every other blob-carrying row.- Bit ordering: P4a's texture-
resource_*bit is to its sampler-view bit what bit 7 is to bit 8 (PipeFill.cpp:1062-1069) — one direction of the A/B is non-functional and must be documented in the same place.