Files
MobileGL/docs/Disaggregated/notes/p4a/scout-frontend-state.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.

1. P6's plan and contract draft (neither started; not one line of code)

     docs/Disaggregated/P6-SPAWN-PLAN.md      the package plan
     docs/Disaggregated/P6-CONTRACT-DRAFT.md  the contract draft (moves to
                                              MG_Remote/CONTRACT-P6.md when c6 lands)

   The three findings that mattered, all written into the documents:

   a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
      the tree and already tested: SocketDoorbell including hangup detection and the two
      lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
      passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
      protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
      SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
      assembly plus a control plane plus a process, not "write a socket transport".

   b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
      audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
      the tree again. So the first package, a6, is a read-only audit that writes no code, and
      every contract row that depends on it is marked PENDING a6 rather than decided in
      advance.

   c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
      and protocol.fbs:186-196's own comment says Android transfers the window out of band -
      that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
      the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
      gate measures. A real window reaching the server is a named refusal. This gets its own
      section because "P6 is done" would otherwise read as "the game runs now".

   The contract adds rule G: a role may not name a process-local handle. It extends P5c's
   rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
   EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
   a token this contract defines or they do not cross, and a descriptor crosses only through
   SCM_RIGHTS, where the kernel renumbers it.

   What P5e left for P6 gets its own section. The latch semantics are already designed
   (ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
   client no longer waits while the peer process can DIE: the client holds records that were
   published and will never be applied, and Android's freezer suspends the child without
   closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
   That is P6's only new design; everything else is carriage. It shares a predicate with E1's
   outstanding re-specification (ID-122), and the two should be designed together.

   Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
   match integration-split's name for name, because a differing set means the lane is quietly
   running less; and the arm-proof gate requires every entry to record the child pid and
   transport=spawn in its OWN private log - ID-124 established that this kind of false green
   really happens, and a spawn lane that actually ran monolith would pass every other gate.
   S2's shape must be settled before it is written, and the document says why: E1 is the
   cautionary case.

2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)

   ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
   the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
   is under no version control. A record cited hundreds of times that lives only locally is not
   a record, so it moves in whole, with its directory structure intact.

   Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
   stay where they are: they are evidence DATA rather than documents - large, regenerated per
   run, and mostly carrying absolute paths and device serials that mean something only on the
   machine that produced them. The mapping and this split are written in
   docs/Disaggregated/notes/README.md.

   Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
   document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
   material salvaged from failed workflow runs, which may duplicate the like-named phase
   directories and is kept because a few of those were the only complete version at the time.

   CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
   deliberately outside it: those notes' file:line citations were written against their own
   code heads, and resolving them against today's HEAD would fail wholesale - which is not an
   error, it is their correct state as a historical record. To check one, use the revision it
   declares (--rev).

3. Inconsistencies fixed in passing

   - README.md's status line still said P5e was in progress while the stage table already said
     closed.
   - CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
     (a) and (b) were run in the previous commit; rewritten to closed plus the one item that
     actually remains (E1).
   - ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
     package order, the scope boundary and the two new gates.

Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
2026-09-18 23:31:39 -04:00

934 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scout — the frontend (MG_State/GLState + MG_Impl/GLImpl) as P4a's client emission must read it
Tree: `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg` @ `37da3c3a`
(feat/disaggregated, P3a closed). Every path below is relative to `MobileGL/` unless it starts
with `docs/`. Every line number was opened in this tree; where a file is large I name the exact
range I read.
Read alongside: `~/w7/notes/p3a/INTEGRATOR-DECISIONS.md` (ID-1..24). The rulings that bind P4a
directly are ID-11/ID-13 (G5 byte-identical functions — none of them are in P4a's files),
ID-12/ID-8b (**the applier keeps object records across make-current; only
`MGPipeApplierReleaseObjectRecords` drops them; the client therefore needs no re-publication on
`FreshlyPrimed`, but must say so as a rule**), ID-18/ID-21 (**leak-at-exit storage: every MGPipe
process singleton is heap-constructed and never destroyed; a new P4a singleton MUST follow**),
and ID-19 (P3a already costs +699/+422 ns/draw; P4a's per-verb emission budget is tight).
---
## 0. The one-paragraph shape
P4a's kinds are **Framebuffer, Texture, Renderbuffer, SamplerCso/SamplerViewCso, ShaderCso**
(`MG_Pipe/MGPipeHandles.h:24-40`). Unlike P3a's buffers, **five of the six already raise a death
notice** and **all six already have a lifetime id**; what none of them has is a client-minted
handle, a client emitter, or an applier record. The frontend versions the emitters need already
exist and are already read by the tracker's shutters (`MG_Impl/Pipe/Tracker.h:303-312`) — bits
11..14 (`NewFramebuffer`, `NewSamplerViews`, `NewSamplers`, `NewShaderImages`) plus bits 6..8
(`NewShader`, `NewShaderBindings`, `NewGlobalConstants`) are **computed, latched and counted
today and emitted by nobody**. P4a is therefore mostly "wire the emitters onto shutters that
already fire", plus four genuinely new pieces of frontend work: the resolved `ReadSurface`, the
framebuffer `Complete` bit, the texture region list on the wire, and the program archive blob.
---
## 1. The object classes: ids, versions, constructors, destructors
### 1.1 Summary table (the hook points)
| kind | class + header | lifetime id | ctor | dtor / death | already raises |
|---|---|---|---|---|---|
| Framebuffer | `FramebufferObject`, `MG_State/GLState/FramebufferState/FramebufferObject.h:105-191` | `m_lifetimeId` `:176`, `GetLifetimeId()` `:165`, allocator `FramebufferObject.cpp:20-24` | `FramebufferObject.cpp:126-135` | `FramebufferObject.cpp:27-37` (`#if MOBILEGL_PIPE_PUSH` only) | `NotifyStateObjectDestroyed(MGPipeKind::Framebuffer, …)` `:36` |
| Texture | `TextureObjectBase : ITextureObject`, `TextureState/TextureObject.h:117-234` | `m_lifetimeId` `:205`, `GetLifetimeId()` decl `:84` / def `TextureObject.cpp:334-336`, allocator `TextureObject.cpp:19,26-28` | `TextureObject.cpp:56-...` | `TextureObject.cpp:31-41` — **virtual, declared on the BASE** so 2D/3D/cube/buffer/view all announce once (`TextureObject.h:120-128`) | `MGPipeKind::Texture` `TextureObject.cpp:40` |
| Renderbuffer | `RenderbufferObject`, `RenderbufferState/RenderbufferObject.h:23-71` | `m_lifetimeId` `:64`, `GetLifetimeId()` `:58`, allocator `RenderbufferObject.cpp:21,24-26` | `RenderbufferObject.cpp:28` | `RenderbufferObject.cpp:31-41` | `MGPipeKind::Renderbuffer` `:40` |
| Sampler | `SamplerObject`, `SamplerState/SamplerObject.h:16-79` | `m_lifetimeId` `:76`, `GetLifetimeId()` `:63`, allocator `SamplerObject.cpp:20-24` | `SamplerObject.cpp:26-27` | `SamplerObject.cpp:30-40` | `MGPipeKind::SamplerCso` `:39` |
| Program | `ProgramObject`, `ProgramState/ProgramObject.h:25-...` | `m_lifetimeId` `:1353`, `GetLifetimeId()` `:1262`, allocator `ProgramObject.cpp:26-30` | inline `ProgramObject.h:55` | `ProgramObject.cpp:32-45` (`CancelLink()` then the notice) | `MGPipeKind::ShaderCso` `ProgramObject.cpp:43` |
| Shader | `ShaderObject`, `ProgramState/ShaderObject.h:24`, dtor `:52` | **NONE** | — | no notice | — |
| ProgramPipeline | `ProgramPipelineObject`, `ProgramState/ProgramPipelineObject.h:19-170` | **NONE** | — | no notice | — (composites are ordinary `ProgramObject`s, §9.4) |
| Buffer (P3a, the model) | `BufferState/BufferObject.cpp` | `g_nextBufferLifetimeId` `:20`, `AllocateLifetimeId` `:23-25` | `:35-47` — **mints the handle unconditionally, emits create only if the subsystem is on** | `:49-65` — `MGPipeEmitResourceDestroyAndFree` returns the latch, and the legacy `OnDestroy` runs only when it says false | emits `resource_destroy` itself (no notice) |
| VAO (P3a, the CSO model) | `VertexArrayState/VertexArrayObject.cpp:41-62` | | | `MGPipeEmitVertexElementsDestroyAndFree(m_lifetimeId)` — **backend-neutral, client-side**, emit → notice → free slot, in that fixed order | |
### 1.2 The two death shapes, and which one each P4a kind takes
`MG_State/GLState/StateObjectDeathNotice.h` (whole file, 62 lines) is the P2 mechanism: one
`StateObjectDeathOps{ void(*OnDestroyed)(MGPipeKind, Uint64 lifetimeId) }` (`:39-43`), a plain
non-atomic global `g_stateObjectDeathOps` (`:45`, written at backend bring-up, read from
destructors — both on the context thread), `SetStateObjectDeathOps` `:47`, and
`NotifyStateObjectDestroyed` `:55-59`. Declares nothing at all in a pull build (`:12`, `:62`).
**The P3a ruling P4a inherits (ID-15 C-1, `MG_Pipe/PipeMutation.h:127-141`):** a death path that
lives only in a backend's death-ops table **is no path at all under a backend that installs
none** — Magma installs none — so every VAO leaked its slot and its ~1.3 KB applier record and
Fatal'd past 65536 slots. The fix was `MGPipeEmitVertexElementsDestroyAndFree`, a **client-side**
helper taking the lifetime id (the object is already gone) that emits `delete_*`, raises the
notice, and frees the slot in that fixed order (`PipeMutation.h:136-141`,
`MG_Impl/Pipe/PipeFill.cpp:801-...`).
**Therefore: for all five P4a kinds the notice at the destructor is NOT sufficient.** Each
destructor must call a client helper of the same shape. The five call sites are the five `#if
MOBILEGL_PIPE_PUSH` destructor bodies listed in 1.1 — they already exist, so P4a edits five
existing lines rather than adding five destructors (which would resize the pull build's symbol
set and break G1).
### 1.3 The per-object version counters P4a's shutters and records need
**Framebuffer** (`FramebufferObject.h`):
- `m_attachmentVersions` `:178`, one `Uint16` per attachment point (`FramebufferAttachmentTypeCount`
= 41 entries, `:24-71`), read whole via `GetAllFramebufferAttachmentVersions()` `:152-154`.
- `m_objectVersion` `:190`, `GetObjectVersion()` `:156` — bumped by `BumpAttachmentVersion`
(`FramebufferObject.cpp:231-235`), by `SetReadBuffer` (`:205-210`) and by the five
`Set Default*` setters (macro `:216-229`). **`SetDrawBuffer` bumps only through
`BumpAttachmentVersion(buffer)` (`:195-199`) — i.e. it versions the *new* attachment slot, not
the draw-buffer array**, which is a real quirk: `glDrawBuffers` writing `NONE` over `COLOR0`
bumps `m_attachmentVersions[None]`, not `[Color0]`.
- `m_drawBuffers` `:180` (`Array<FramebufferAttachmentType, MAX_DRAW_BUFFERS=kMGMaxDrawBuffers>`,
`:107,113`), `m_readBuffer` `:181`, defaults seeded in the ctor `:126-135`
(`BackLeft` for name 0, `Color0` otherwise).
- `FramebufferAttachmentObject` `:74-103` is the attachment struct: `SharedPtr<ITextureObject>`
**or** `SharedPtr<RenderbufferObject>` (`:96-97`), `m_textureUploadTarget` `:98`, `m_textureLevel`
`:99`, `m_textureLayer` `:100`, `m_layered` `:101`, `m_isValid` `:102`. `IsComplete()`
`FramebufferObject.cpp:89-99`, `GetSize()` `:101-119` (resolves `Unknown` upload target to
`GetUploadTargets()[0]`).
- **`FramebufferState`** `FramebufferState/FramebufferState.h:15-45`: `m_framebufferObjects`
(`UnorderedMap<Uint, SharedPtr<…>>`) `:41`, `m_bindingSlots[Draw|Read]` `:43-44`,
`NoteAttachmentChanged`/`GetAnyAttachmentGeneration` `:33-34` under `#if MOBILEGL_PIPE_PUSH`
`:28-40`. `MarkFramebufferObjectForDeletion` `FramebufferState.cpp:59-72` rebinds any slot
holding the victim to FBO 0 and erases the map entry.
**Texture** (`TextureState/TextureObject.h`): `ITextureObject` `:23-115` is the interface (39
pure virtuals); `TextureObjectBase` `:117-234` the storage-free base; `TextureObjectMipmap`
`:236-303` adds the level API; `TextureObjectWithOneMipmap` `:344-378` holds
`MipmapUploadTargetArray<1> m_textureStorage` `:377`. Concrete leaves: `TextureObject1D/2D/3D.h`,
`TextureObject2DCube.h`, `TextureObjectBuffer.h`, `TextureObjectView.h`.
Three independent counters, all on the base:
- `m_textureParamsVersion` (Uint16) `:213`, `GetTextureParamsVersion()` `:158` — the
glTexParameter family.
- `m_shapeVersion` (Uint64, **starts at 1**) `:217`, `GetShapeVersion()` `:160`, bumped **only**
by `BumpShapeVersion()` (`:202`, def `TextureObject.cpp:44-54`), which also calls
`pGLContext->BumpSamplingResolutionGeneration()` `:53`. Invalidates the completeness memo
`m_completeMemoShapeVersion[2]` / `m_completeMemoValue[2]` `:220-221`, read by
`IsMipmapCompleteForFilterCached` `TextureObject.cpp:298-307`.
- `m_contentVersion` (Uint64, **starts at 1**) `:224`, `GetContentVersion()` `:159`, bumped by
`MarkStorageDirty(dirty=true)` (`TextureObject.cpp:379-386`), `MarkStorageDirtyRegion`
(`:392-398`) and `BumpContentVersion()` (`:309-312`).
- Other state the descriptor needs: `m_internalFormat` `:207`, `m_sampler` `:208` (**every texture
owns a `SamplerObject(0)`**, `TextureObject.cpp:58`), `m_swizzleParams` `:209`, `m_levelRange`
`:211` (`{0,1000}`), `m_immutableLevels` `:212`, `m_depthStencilTextureMode` `:225`,
`m_viewMinLevel/NumLevels/MinLayer/NumLayers` `:228-231`, `m_samples` `:232`,
`m_fixedSampleLocations` `:233`.
- Views: `TextureObjectView.h:44-135`, `m_storageOwner` `:134`, `GetViewStorageOwner()` override
`:49`; the base returns a static null `TextureObject.cpp:338-343`. `IsTextureView()`
`TextureObject.h:102`. A view is composed onto the ROOT at creation, so one hop always reaches
storage (`:90-101`) — that is exactly `MGPResourceDesc::ViewOf`.
- Texture buffers: `TextureObjectBuffer.h:16-48` — `m_bufferBindingSlot` `:46`,
`m_bufferRangeOffset` `:47`, `m_bufferRangeSize` `:48` with `kWholeBuffer = ~0` `:28`,
`GetBufferRangeSizeInBytes()` `:35-41` **resolves live against the buffer's current size** —
which is exactly the `kMGPipeWholeBuffer` / "resolved live" contract of
`MGPipeTypes.h:173-177`.
- `TextureState` `TextureState/TextureState.h:39-173`: `MAX_TEXTURE_IMAGE_UNITS = 192` `:42`,
`MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32` `:47`; `m_textureUnits` `:166`,
`m_imageTextureBindings` `:167`, `m_textureObjects` `:169`, `m_defaultTextureObjects` `:172`
(name 0, one per target, immortal). `ImageTextureBinding` `:18-37` = `{Texture, Level, Layered,
Layer, Access, Format, Version}` with `Bind()` `:27-36` bumping `Version`.
- `TextureUnit` `TextureState/TextureUnit.h:15-26`: `m_slots[TextureTargetCount]` `:24` +
`m_sampler` `:25`; `SetSamplerObject` `TextureUnit.cpp:28-40` bumps
`pGLContext->BumpTextureBindGeneration()` `:39`.
**Renderbuffer** — **the outlier: no version at all.** `RenderbufferObject.h:63-70` has
`m_externalIndex`, `m_lifetimeId`, `m_internalFormat`, `m_width`, `m_height`, `m_samples`,
`m_allocated`, `m_componentSizes` and nothing else. `SetInternalFormat`
(`RenderbufferObject.cpp:96-99`), `AllocateStorage` (`:101-105`) and `SetSamples` (`:107-109`)
write the members and **bump nothing and notify nothing**. See §5.2 — this is a real P4a hole.
**Sampler** — `m_version` (Uint16) `SamplerObject.h:77`, `GetVersion()` `:58`;
`BumpVersion()` `:73` (def `SamplerObject.cpp:43-54`) is the **only** writer, called by all 14
setters (`:56-...`), each of which early-outs on an unchanged value. It does three things:
`++m_version`, `pGLContext->BumpSamplingResolutionGeneration()` (`:50`), and
`MGP_NOTE_AGGREGATE(TextureParams)` (`:53`). `m_samplerParameters` `:78` is the whole
`SamplerParameters` POD that `MGPSamplerDesc::Parameters` ships byte-for-byte
(`MGPipeTypes.h:283-287`; `docs/Disaggregated/ARCHITECTURE.md:4.1` insists `borderColorForm`
travels with it). `SamplerState` `SamplerState.h:17-31` is just a map + `IndexGenerator`.
**Program** — five counters, all read by the tracker (`Tracker.h:251-260`):
- `m_linkVersion` (mutable Uint32) `ProgramObject.h:1418`, `GetLinkVersion()` `:793` — bumped
**only** by `BumpLinkObservableVersions()` (decl `:1340`), called at Link()'s prologue and at
both publish sites (`ProgramObject.cpp:72`, `:119`).
- `m_backendStateVersion` (mutable) `:1399`, `GetBackendStateVersion()` `:790`.
- `m_blockBindingVersion` `:1401`, `GetBlockBindingVersion()` `:1013`, bumped by
`SetUniformBlockBinding` `:1020-1027` (and the storage-block setter).
- `m_imageUnitVersion` `:1394`, `GetImageUnitVersion()` `:855`, bumped inside
`SetUniformSamplerOrImageUnitIndex` **only for image uniforms** `:847-849`.
- `m_uniformWriteSetVersion` `:1393`, `GetUniformWriteSetVersion()` `:601`.
- `m_uboContentVersion` (mutable) `:1417`, `GetUBOContentVersion()` `:740`,
`MarkUBOContentDirty()` `:741-743` — **`~0u` is reserved as the backends' "never uploaded"
sentinel and the wrap skips it**.
- `m_artifacts` `:1426` and `m_spirv` `:1429` are private and reachable only through
`Artifacts()` / `Spirv()`, which join `m_pendingLink` `:1434` / `m_pendingSpirv` `:1438`
(invariant I5, `:1264-1275`).
---
## 2. The mutation entry points, per family
Frontend GL entry points live in `MG_Impl/GLImpl/<Family>/GL_<Family>.cpp` in a **two-layer
shape**: a `Foo_State(...)` function that mutates `MG_State` and validates, and a public `Foo(...)`
that calls it then dispatches through `MG_Backend::gBackendFunctionsTable.GL.Foo` behind an
`MGP_FILL(Verb)`. P4a's emission sites are the `_State` bodies (mutation) and, for the verbs,
the `MGP_FILL` point.
### 2.1 Framebuffer — `MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp` (182 KB), decls
`GL_Framebuffer.h:16-96`
- Attach: `FramebufferTexture{,1D,2D,3D,Layer}` `GL_Framebuffer.h:44-49`,
`NamedFramebufferTexture{,1D,2D,3D,Layer}` `:50-55`, `FramebufferRenderbuffer` `:34`,
`NamedFramebufferRenderbuffer` `:35-36`. All land in `FramebufferObject::AttachTexture`
(`FramebufferObject.cpp:137-142`) / `AttachRenderbuffer` (`:144-148`) / `Detach` (`:150-153`),
each ending in `BumpAttachmentVersion` (`:231-235`) → `++m_attachmentVersions[type]`,
`++m_objectVersion`, `MGP_NOTE_AGGREGATE(FramebufferAttachment)`.
- Draw/read buffers: `DrawBuffer` / `DrawBuffers` / `ReadBuffer` `:78-80` and their `Named*`
forms `:56-58`. Implementations: `DrawBuffersForFramebuffer_State`
`GL_Framebuffer.cpp:1719-1830`, `DrawBuffers_State` `:1831-1836`, `DrawBuffer_State`
`:1838-1848`, `ReadBufferForFramebuffer_State` `:1850-1899` (ends at `fbo->SetReadBuffer(attType)`
`:1898`), `ReadBuffer_State` `:1901-1906`, `NamedFramebufferDrawBuffers_State` `:1908-1916`.
- Default geometry (`ARB_framebuffer_no_attachments`): `FramebufferParameteri` `:71`,
`NamedFramebufferParameteri` `:73` → the five setters `FramebufferObject.h:146-150`, defined by
the macro at `FramebufferObject.cpp:216-229`.
- Renderbuffer storage: `RenderbufferStorage` / `RenderbufferStorageMultisample` `:23-25`,
`NamedRenderbufferStorage{,Multisample}` `:30-32`; bodies
`RenderbufferStorageMultisample_State` `GL_Framebuffer.cpp:817-842`,
`AllocateRenderbufferStorage_State` `:844-858`, `RenderbufferStorage_State` `:860-866`.
**None of them notify anything** (§5.2).
- `BindFramebuffer` `:85`, `BindRenderbuffer` `:38`, `DeleteFramebuffers` `:81`,
`DeleteRenderbuffers` `:37`, `Blit{,Named}Framebuffer` `:75-77,83-84`, `Invalidate*` `:61-66`,
`CheckFramebufferStatus` `:82` / `CheckNamedFramebufferStatus` `:69`, `ReadPixels`/`ReadnPixels`
`:16-18`, `ClearBuffer*` `:19-22` / `ClearNamedFramebuffer*` `:59-60,67-68`.
- `DefaultFramebufferInfo` `GL_Framebuffer.h:87-96` holds the default FBO plus its three synthetic
attachments; `pDefaultFramebufferInfo->defaultFBO` is the identity every "is this the default"
test compares against (`GL_Framebuffer.cpp:1834`, `:1844`, `:1904`). This is exactly the four
comparisons `MGPipeHandles.h:67-73` retires into `kMGPipeDefaultFramebuffer{0,1}`.
### 2.2 Texture — `MG_Impl/GLImpl/Texture/GL_Texture.cpp` (426 KB), decls `GL_Texture.h`
Storage / respecify (each → `MGPResourceDesc` create-or-respecify):
- `TexImage3D_State` `:2352`, `TexImage2D_State` `:2524`, `TexImage1D_State` `:2678` — each
`AllocateStorage(...)` (`:2473`, `:2621`, `:2734`), optional `UpdateMipmapSubData` (`:2515`,
`:2668`, `:2754`), then `MarkStorageDirty(..., true)` (`:2518`, `:2674`, `:2758`).
- `TexImage2DMultisample_State` `:2308`, `TexImage3DMultisample_State` `:2264`.
- `CompressedTexImage{1,2,3}D_State` `:4984`, `:4904`, `:4830` (`AllocateStorage` `:4895`/`:4975`,
`MarkStorageDirty` `:4901`/`:4981`).
- `TexStorage{1,2,3}D` `:6002`, `:6027`, `:6052` — allocate every level with
`MarkStorageDirty(..., false)` (`:5554`, `:5628`, `:5698`) then `TruncateMipmapLevels`
(`:5564`, `:5644`, `:5714`). `TexStorage{2,3}DMultisample` `:6110`, `:6121`, shared validator
`TexStorageMultisample_State` `:6098`.
- DSA `TextureStorage*` / `TextureSubImage*` / `CompressedTextureSubImage*` — `GL_Texture.h:41-60`.
- `TextureView` `:5838` (decl `GL_Texture.h:83`) → `TextureState::CreateTextureViewObject`
(`TextureState.h:55-58`).
- `TextureBuffer` / `TextureBufferRange` `GL_Texture.h:73-74`.
- The generated-mip storage grow: `GL_Texture.cpp:528-539` (`AllocateStorage`,
`MarkStorageDirty(false)`, `TruncateMipmapLevels`, then **`BumpContentVersion()` `:539`** —
the level SET grew without any CPU byte being dirty).
Sub-image (each → `MGPSubData` + regions):
- `TexSubImage3D_State` `:1725` → `MarkStorageDirtyRegion(target, level, {x,y,z}, {w,h,d})`
`:1820-1821`.
- `TexSubImage2D_State` `:1825` → `MarkStorageDirtyRegion(..., {x,y,0}, {w,h,1})` `:1940`.
- `TexSubImage1D_State` `:1945` → `:2007`.
- `CompressedTexSubImage{1,2,3}D_State` `:4818`, `:4658`, `:4489`.
- `ClearTexImage` `:1102` / `ClearTexSubImage` `:1118` → `MarkStorageDirty(..., true)` `:1006`,
`:1097`.
- Copy family: `CopyTexImage2D_State` `:4389` / `_Backend` `:4467`, `CopyTexSubImage2D_Backend`
`:4028`, `CopyTexSubImage3D_State` `:3986`, `CopyTexSubImage1D_State` `:4372`,
`CopyTexImage1D_State` `:4474`, DSA `CopyTextureSubImage{1,2,3}D` `GL_Texture.h:133-136`.
- `CopyImageSubData` `GL_Texture.h:143`, validator `ValidateCopyImageSubData_State`
`GL_Texture.cpp:4263`, backend hop `CopyImageSubData_Backend` `:4034` (verb
`CopyImageSubData`, class `kBlitOrCopy`, `MG_Pipe/FillPoints.def:113`) — the payload is
`MGPCopyRegion` (`MGPipeTypes.h:708-716`).
Parameters (each → `MGPTextureParams`, or the sampler-owned half):
- `TexParameter{f,i,fv,iv,Iiv,Iuiv}_State` `GL_Texture.cpp:2012`, `:2110`, `:2126`, `:2163`,
`:2194`, `:2224`; DSA `TextureParameter*` `GL_Texture.h:62-67`.
- They land on `TextureObjectBase::Set*`, each of which bumps `m_textureParamsVersion` and fires
`MGP_NOTE_AGGREGATE(TextureParams)`: `SetInternalFormat` `TextureObject.cpp:98-119` (also
`BumpShapeVersion` `:116` and a `BumpTextureBindGeneration` for the default-texture
Unknown↔defined transition `:108-113`), `SetBorderColor` `:136-145`, `SetBorderColorI`
`:151-160`, `SetBorderColorUI` `:166-175`, `SetSwizzleParam` `:202-225`, `SetSwizzleParamRGBA`
`:227-233`, `SetBaseLevel` `:239-252` (+`BumpShapeVersion` `:251`), `SetMaxLevel` `:254-264`
(+`:263`), `SetImmutableLevels` `:274-284`, `SetSamples` `:318-322`, `SetFixedSampleLocations`
`:328-332`, `SetDepthStencilTextureMode` **inline in the header** `TextureObject.h:188-193`.
- **TEXTURE_BORDER_COLOR / MIN_FILTER / WRAP_* are NOT texture-object state**: they live on the
texture's own `SamplerObject` (`TextureObject.cpp:125-179`), so a `glTexParameteri(GL_TEXTURE_
MIN_FILTER)` runs `SamplerObject::BumpVersion` and fires `MGP_NOTE_AGGREGATE(TextureParams)`
through *that* path.
Mipmap generation: `GenerateMipmap` `GL_Texture.cpp:6690-6709` (`EnsureGeneratedMipmapStorage
Allocated` `:6707` then `GenerateMipmap_Backend` `:1623`), `GenerateTextureMipmap` `:6711-6720`
(binds temporarily, `WithTemporarilyBoundNamedTexture`), guard `ValidateGenerateMipmapTexture`
`:6675-6688`, auto-gen hook `MaybeAutoGenerateMipmap` `:1628`. Verb `GenerateMipmap`, class
`kTextureOp` (`FillPoints.def:114`), payload `MGPMipPlan` (`MGPipeTypes.h:741-745`).
Binds: `BindTexture_State` `:4995`, `BindTextureUnit` `:6296`, `BindTextures` `:6395`,
`BindImageTexture` `:6612-6670`, `BindImageTextures` `:6418`. **`BindImageTexture` is the one
that is already a verb** (`FillPoints.def:122`, class `kTextureOp`): it writes
`GetImageTextureBinding(unit).Bind(...)` `:6665-6666`, calls `NoteTextureUnitTouched(unit)`
`:6667`, then `MGP_FILL(BindImageTexture)` `:6668` and dispatches `:6669`.
`glBindTexture`/`glBindSampler` are **not** verbs — they route through
`TextureState::NoteUnitTouched` (`TextureState.h:78-100`) only.
Readback: `GetTexImage_State` `:5251` / `_Backend` `:5079`, `GetCompressedTexImage_State` `:3801`,
`GetTextureSubImage` `GL_Texture.h:75-76`, `CopyTextureImageToClientOrPBO_State`
`GL_Texture.cpp:5375`.
### 2.3 Sampler — `MG_Impl/GLImpl/Sampler/GL_Sampler.cpp` (27 KB), decls `GL_Sampler.h:14-29`
`SamplerParameter{f,i,fv,iv,Iiv,Iuiv}` `:15-20`, `BindSampler` `:29`, `BindSamplers` `:28`,
`GenSamplers` `:25`, `CreateSamplers` `:27`, `DeleteSamplers` `:26`, the six `Get*` queries.
All setters funnel to `SamplerObject::Set*` → `BumpVersion()` (§1.3). `glDeleteSamplers` goes
through `GLContext::MarkSamplerObjectForDeletion` (`Core.cpp:1228-1240`), which **unbinds the
sampler from all 192 units first** (`:1232-1237`) so `TextureUnit::SetSamplerObject`
(`TextureUnit.cpp:28-40`) bumps the bind generation.
### 2.4 Program / pipeline — `MG_Impl/GLImpl/Program/GL_Program.cpp` (188 KB) +
`GL_ProgramPipeline.cpp` (12.7 KB) + `ProgramInterface.cpp` (49 KB)
- `UseProgram_State` `GL_Program.cpp:1451-1481` (public `UseProgram` `:2530`) → refuses while XFB
is active-and-unpaused `:1457-1464`, refuses an unlinked program `:1473-1479`, then
`pGLContext->UseProgram(program)` `:1480` → `ProgramState::UseProgram` (`Core.cpp:393-395`).
- `LinkProgram_State` `:1395` (public `:2522`), `DeleteProgram_State` `:606` (public `:2434`).
- **The one uniform funnel: `Uniform_State<ItemCount,T>` `GL_Program.cpp:1483-1561`.** Every
`glUniform*` and `glProgramUniform*` reaches it once per LOCATION (`:1495-1496`). Non-opaque
arm: `MarkUniformWrittenAtLocation` `:1499` → `BufferUniformWrite` if SPIR-V is pending
`:1516-1519` → `MapUBO()`/`GetUBOOffset` `:1520-1532` → **bytes-equal dedupe `:1542`** →
`Memcpy` `:1543` → `MarkUBOContentDirty()` `:1544`. Opaque arm `:1545-1560`:
`SetUniformSamplerOrImageUnitIndex(location, unit)` `:1559`, which bumps
`m_imageUnitVersion` only for image uniforms (`ProgramObject.h:847-849`).
Wrappers: `Uniformv_State` `:1563-1587` (reads `GetProgramForUniform()` `:1567`),
`ProgramUniformv_State` `:1590`. Public entry points from `:2534` (`Uniform1f`), `:2553`
(`Uniform1i`), `:2999` (`ProgramUniform1i`), etc.
- `glUniformBlockBinding` → `ProgramObject::SetUniformBlockBinding` `ProgramObject.h:1020-1027`
(bumps `m_backendStateVersion` **and** `m_blockBindingVersion`).
- `glShaderStorageBlockBinding` is a **verb** (`FillPoints.def:124`, class `kProgramOp` — the
class exists solely because it syncs Espryt's render state and textures, `:150-151`, `:323-334`).
- Pipelines: `GL_ProgramPipeline.cpp`; frontend state `Core.h:200-215`
(`GenProgramPipelineNames` `:204`, `CreateProgramPipelineObject` `:205`,
`BindProgramPipelineObject` `:208` → `Core.cpp:1391`, `MaterializeProgramPipelineObject` `:211`,
`MarkProgramPipelineForDeletion` `:212`, `GetBoundProgramPipeline` `:215` → `Core.cpp:1429`,
`m_boundProgramPipeline` `Core.h:660`).
### 2.5 Pixel store — `glPixelStore{i,f}`
`GLContext::SetPixelStoreParam` `Core.cpp:1134-1136` → `RenderState::SetPixelStoreParam`
`RenderState/RenderState.cpp:828-856` (a 16-arm macro switch, Pack ×8 + Unpack ×8).
`GetPixelStoreParameters(Bool isUnpack)` `RenderState.cpp:885-887`. Storage:
`m_pixelStorePackParameters` / `m_pixelStoreUnpackParameters` `RenderState.h:180-181` — **deliberately
outside `RenderStateParameters m_parameters` `:177`, so they are NOT part of the CSO byte range,
and the setter bumps NO version** (`SetPixelStoreParam` never calls `BumpVersions()` `:159-162`).
---
## 3. Which mutation sites already carry a `MGP_NOTE_MUTATION` / `MGP_NOTE_AGGREGATE`
Macros: `MG_Pipe/PipeMutation.h:155-158` (push) / `:160-161` (pull no-ops).
`MGPipeAggregate` enum `:56-74`. Dispatch: `MG_Impl/Pipe/PipeFill.cpp:529-552`
(`MGPipeNoteAggregate`) and `:509` (`MGPipeNoteFrontendMutation`).
**Complete census of the P4a-relevant sites** (grep over `MG_State/` + `MG_Impl/`):
`MGP_NOTE_AGGREGATE(FramebufferAttachment)` — 3 sites, all in
`FramebufferState/FramebufferObject.cpp`: `:209` (`SetReadBuffer`), `:221` (the five default-geometry
setters via macro), `:234` (`BumpAttachmentVersion`, i.e. every attach/detach/draw-buffer write).
`MGP_NOTE_AGGREGATE(TextureParams)` — 13 sites: `TextureObject.cpp:118, 144, 159, 174, 224, 232,
250, 262, 283, 321, 331`; `TextureObject.h:192` (`SetDepthStencilTextureMode`);
`SamplerState/SamplerObject.cpp:53` (`BumpVersion`, the choke point for **all** sampler params).
`MGP_NOTE_AGGREGATE(TextureContent)` — 5 sites: `TextureObject.cpp:311` (`BumpContentVersion`),
`:383` (`MarkStorageDirty`), `:395` (`MarkStorageDirtyRegion`);
`TextureState/TextureObject2DCube.cpp:53, 65` (the cube overrides).
`MGP_NOTE_MUTATION(...)` — **only four sites in the whole tree, all in
`TextureState/TextureState.h`**: `:85` `GetMaxTouchedTextureUnit`, `:98` and `:111`
`GetTextureBindGeneration`, `:133` `GetSamplingResolutionGeneration`. (P3a widened
`gen_pipe_dirty_surface.py`'s scan root to `MG_State/GLState` precisely to bring these inside the
gate — `MG_Pipe/DirtySurface.def:150-153`.)
**Nothing at all fires for**: any `RenderbufferObject` mutation, any `ProgramObject` version bump,
`glUseProgram`, `glBindProgramPipeline`, `glBindImageTexture`'s `ImageTextureBinding::Bind`
(only `NoteUnitTouched` runs), and `SetPixelStoreParam`.
---
## 4. The dirty bits P4a inherits — already computed, emitted by nobody
`MG_Impl/Pipe/Tracker.h`. Enum `MGPipeDirty` `:57-84`; names `:106-125`;
`MGPipeSubsystemForDirty` `:130-154` (**returns 0 for every bit past 10** `:149-153`);
`MGPipeMixShutter` `:170-173`; `MGPipeWidenedCounter` `:186-204`.
Shutters computed in `MGPipeTracker::Update` `:215-361`:
| bit | name | shutter, verbatim | line |
|---|---|---|---|
| 6 | `NewShader` | `Mix(program->GetLifetimeId(), program->GetLinkVersion())` | `:252`, stored `:261` |
| 7 | `NewShaderBindings` | `Mix(Mix(Mix(ImageUnitVersion, BackendStateVersion), BlockBindingVersion), UniformWriteSetVersion)` | `:253-257`, `:262` |
| 8 | `NewGlobalConstants` | `Mix(LifetimeId, UBOContentVersion)` | `:258`, `:263` |
| 11 | `NewFramebuffer` | `Mix(ctx.GetAnyFramebufferAttachmentGeneration(), m_framebufferBind.Observe(ctx.GetFramebufferBindingSlot(Draw).GetVersion()))` | `:303-306` |
| 12 | `NewSamplerViews` | `Mix(textureContent, ctx.GetTextureBindGeneration())` | `:307-308` |
| 13 | `NewSamplers` | `Mix(textureParams, ctx.GetSamplingResolutionGeneration())` | `:309-310` |
| 14 | `NewShaderImages` | `Mix(Mix(textureContent, textureParams), program->GetImageUnitVersion())` | `:311-312` |
`textureContent` / `textureParams` are read at `:266-267`. The program is read from
`ctx.GetCurrentProgram()` `:246` **and deliberately not `GetProgramForDraw()`** — "the tracker must
not force a compile just to answer did the shader move" `:242-245`. **P4a must respect that**: the
emitter, which needs the artifacts, is the thing that joins.
Also: `m_primed` / `FreshlyPrimed()` `:349-350`, `:417-420`; `Reset()` `:378-392` (called from
`Update` whenever `m_context` moves `:219-222`); the fire tallies `:353-359`.
`kMGPipeDirtyEmittedAtP2` `:96-99` and `kMGPipeDirtyEmittedAtP3a` `:102-104` — **P4a adds a
`kMGPipeDirtyEmittedAtP4a` constant rather than editing either** (the rule is stated at `:93-95`).
Subsystem bits: `MG_Pipe/MGPipe.h:72-95`; `kMGPipeSubsystemResources = 1<<7` `:84`,
`kMGPipeSubsystemVertexInput = 1<<8` `:85`, `kMGPipeSubsystemsMigratedAtP3a = 0x1ff` `:95`.
Bit 63 is the CSO content-addressing negative control `:90`. **Bits 9+ are free for P4a.**
---
## 5. The dirty-surface rows P4a's kinds already carry, and the two holes
`MG_Pipe/DirtySurface.def`. Legend `:101-143` (`kImmediate`, `kReverseChannel`, `kNoBackendRead`,
`kExplicitDestroy`, `kUnpublishedDestroy`, `kPulledEveryVerb`, `kPulledPartialShutter|BIT`).
Gate: `scripts/gen_pipe_dirty_surface.py --check` / `--self-test`, scan root now **both**
`MG_Impl/GLImpl` and `MG_State/GLState`, and a `MGP_NOTE_MUTATION` site counts as a publish
mechanism whose "mutator" is the enclosing function (`:16-28`, `:145-155`).
Rows P4a touches:
- `X(BumpTextureBindGeneration, NEW_SAMPLER_VIEWS)` `:248`
- `X(BumpSamplingResolutionGeneration, NEW_SAMPLERS)` `:256` (added at P3a by the widened root)
- `X(NoteUnitTouched, kPulledPartialShutter|NEW_SAMPLER_VIEWS)` `:271` — the bind generation moves
only on the `bindingChanged` arm; `GetMaxTouchedTextureUnit` has **no shutter at all** `:265-266`
- `X(SetPixelStoreParam, kPulledPartialShutter|NEW_PIXEL_PACK)` `:237` with the reasoning at
`:224-236`: the setter writes BOTH halves, only the pack half has a bit, so
`glPixelStorei(GL_UNPACK_ALIGNMENT, 8)` **moves nothing at all**
- `X(SetActiveTextureUnit, kImmediate)` `:164`
- Deaths `:299-307`: `MarkFramebufferObjectForDeletion`, `MarkRenderbufferObjectForDeletion`,
`MarkSamplerObjectForDeletion`, `MarkTextureObjectForDeletion` are all `kExplicitDestroy`;
**`MarkProgramForDeletion`, `MarkProgramPipelineForDeletion`, `MarkShaderForDeletion` are
`kUnpublishedDestroy` `:301-302, :305`** — a recorded hole ("programs, program pipelines and
shaders have no per-object handle on the wire at all in P2", `:113-124`). **P4a closes the
program row and must decide explicitly what to do with pipeline and shader** (a pipeline has no
wire object; a composite does — see §9.4).
- `MGP_DIRTY_SURFACE_UNDECIDED_LIST` `:332` is **empty**; every bit answer above is derived and
checked in both directions.
### 5.1 Hole A — `glUseProgram` has no row and no publisher
`GLContext::UseProgram` (`Core.cpp:393-395`) writes `ProgramState::m_currentProgram`
(`ProgramState.h:100`). The scanner sees `pGLContext->UseProgram(...)` from
`GL_Program.cpp:1467`/`:1480`, so a row is owed. Bit 6's shutter mixes lifetimeId ⊕ linkVersion
(`Tracker.h:252`), so a switch between two programs DOES move it — the row's honest answer is
`NEW_SHADER`, but **it is not there today**, meaning the `--check` gate will fail the moment the
scan is re-run against a `UseProgram` call site it now recognises. Verify before writing the brief.
### 5.2 Hole B — a renderbuffer respecify publishes nothing
`RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples}`
(`RenderbufferObject.cpp:96-109`) bump no version and raise no notice, and
`AllocateRenderbufferStorage_State` (`GL_Framebuffer.cpp:844-858`) adds nothing. Bit 11's shutter
(`Tracker.h:303-306`) is `anyAttachmentGeneration ⊕ drawFboBindVersion` — neither moves when an
**already-attached** renderbuffer is re-storaged. So
`glBindRenderbuffer; glRenderbufferStorage(newsize)` on a renderbuffer that is already an FBO
attachment is invisible to the framebuffer bit. P4a needs either (a) a version + aggregate on
`RenderbufferObject` (matching `TextureObjectBase::BumpShapeVersion`, which does exactly this and
routes through `BumpSamplingResolutionGeneration`), or (b) `resource_respecify` emitted straight
from the storage entry points the way `BufferObject::NotifyRespecify` does
(`BufferObject.cpp:67-79`) — option (b) matches ARCHITECTURE's rule that renderbuffers share
`MGPResourceDesc` (`docs/Disaggregated/ARCHITECTURE.md` §4.1 table).
---
## 6. The pixel-store shadows — two of them, on opposite sides
### 6.1 Frontend: unpack is fully resolved client-side, pack is not
Every texture upload calls
`MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(pixels,
pGLContext->GetPixelStoreParameters(true), …)` before touching the level shadow — call sites
`GL_Texture.cpp:905, 1780-1782, 1902, 1991-1992, 2502-2503, 2655-2656, 2749-2750, 6195-6196`
(decl `MG_Util/Texture/PixelStoreProcessor.h:16`, def `PixelStoreProcessor.cpp:858`). The result
is a **tightly packed** buffer copied row by row into the level shadow
(`GL_Texture.cpp:1803-1817`). This is why `docs/Disaggregated/ARCHITECTURE.md:` §3.2's "explicitly
not migrated" list contains `set_pixel_unpack_state` ("前端已在 `glTexImage` 时解析压缩格式、强制默
认 unpack") and why `MGPPixelPackState` is PACK-only (`MGPipeTypes.h:531-542`).
**Consequence for P4a: `MGPSubData`'s bytes are already tight; `MGPSubRegion::SrcRowStride /
SrcSliceStride` (`MGPipeTypes.h:611-612`) describe the LEVEL SHADOW's pitch, not the
application's.**
Pack, by contrast, is read live at readback: `GL_Texture.cpp:5356`, `:5411`,
`DirectGLES.cpp:6593`, `:8043`, `:9530`, `:9909`.
### 6.2 Espryt: two process-wide driver shadows, asymmetrically invalidated — the pollution risk
- **UNPACK**: `ScopedDefaultUnpackState`, `MG_Backend/DirectGLES/Managers.cpp:4851-4918`. Shadow
statics `s_synced, s_alignment, s_rowLength, s_skipRows, s_skipPixels, s_imageHeight,
s_skipImages` at `:4911-4917` — **`static inline` class members, i.e. one set per PROCESS, not
per context**. `EnsureShadowSynced()` `:4874-4891` pins the driver to GL defaults exactly once
and sets `s_synced = true` `:4878`. **`s_synced` is written at `:4878` and read at `:4875` and
nowhere else in the file — there is no invalidation path at all.** The class comment `:4842-4850`
states the invariant it depends on: "The backend unpack state is set ONLY by MobileGL's own
save/restore helpers … so the shadow stays accurate". Any glPixelStorei on an `UNPACK_*` from
outside those helpers, or a driver context whose unpack state is not the pinned default,
desynchronises it permanently. The known outside writer is the ring path's
`glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x())` at `Managers.cpp:6400` (guarded by
`!ringStaged`, comment `:6297`).
- **PACK**: `PixelStoreImpl`, declared `MG_Backend/DirectGLES/Managers.h:1783-1806`
(`struct PackState{Alignment,RowLength,SkipRows,SkipPixels}` `:1793-1802`), defined
`Managers.cpp:8004-8054` — file-scope `g_packState` / `g_packStateKnown` `:8006-8007`,
`PinPackState` `:8009-8016`, `ApplyPackState` `:8019-8040`, `CurrentPackState` `:8042-8049`,
`InvalidatePackStateCache` `:8051-8053`. **This one IS invalidated**, at MakeCurrent
(`DirectGLES.cpp:10578`) and at context destroy (`:11112`). `ScopedPackState` RAII
`DirectGLES.cpp:6577-6588`, `PackStateFromContext` `:6592-6593`; users at `:7092, 8477, 8588,
8683, 9103, 9432`.
- Header note `Managers.h:1789-1791`: `PACK_IMAGE_HEIGHT / SKIP_IMAGES / SWAP_BYTES / LSB_FIRST`
have **no ES equivalent** and are honoured on the CPU from frontend context state — i.e. the
boundary must keep carrying the full `PixelStoreParameters` (28 B, `MGPipeTypes.h:538-542`),
not the four ES-expressible fields.
**P4a's stake**: once texture upload is emitted by handle, Espryt's upload path stops being the
thing that reads the frontend's unpack state (ARCHITECTURE.md:252 says it "改为从描述符取步长,
`UNPACK_ROW_LENGTH` 从 `SrcRowStride/bpp` 设"), so the P4a emitter has to carry the strides that
`ScopedDefaultUnpackState`'s pin currently makes unnecessary — and the asymmetric invalidation
above becomes a correctness dependency rather than a performance one.
---
## 7. `ReadSurface` resolution and framebuffer completeness
`MGPFramebufferState` (`MG_Pipe/MGPipeTypes.h:355-372`, 304 B) demands three things the client
must resolve **before** the record goes out:
- `MGPSurface ReadSurface` `:361` — "The RESOLVED read surface, not an index. This is what
structurally closes the read-buffer-shared-FBO defect class."
- `Uint8 Complete` `:364`.
- `Uint64 ContentHash` `:370` — both the server's render-pass memo key and the client's emission
suppressor.
`MGPSurface` itself `:343-353` (24 B): `{Res, InternalFormat, Kind(Texture|Renderbuffer|None),
Layered, Level, Layer, UploadTarget}` — **`InternalFormat` is inline so four cross-object masks
fall out with zero lookups** (`:341-342`).
**Where the client gets each piece:**
- Bound FBO: `ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw|Read)`
(`Core.h:547`, `FramebufferState.cpp:49-57`); default-FBO identity is
`FramebufferImpl::pDefaultFramebufferInfo->defaultFBO` (`GL_Framebuffer.h:89, 95`).
- Attachments: `fbo->GetAllAttachmentObjects()` (`FramebufferObject.h:131`,
`FramebufferObject.cpp:159-161`) or per point `GetAttachment(type)` `:155-157`;
then `IsTexture()`/`IsRenderbuffer()`/`GetTexture()`/`GetRenderbuffer()`/`GetTextureLevel()`/
`GetTextureLayer()`/`IsLayered()`/`GetTextureUploadTarget()`
(`FramebufferObject.cpp:53-87`), format from `ITextureObject::GetFormat()` or
`RenderbufferObject::GetInternalFormat()`.
- **The read surface**: `fbo->GetReadBuffer()` (`FramebufferObject.h:137`) then
`fbo->GetAttachment(readBuffer)` — this is the exact two-step every consumer already does:
frontend `GL_Framebuffer.cpp:2373, 2921, 2992`, `Framebuffer/Validators.cpp:185`,
`Getter/GL_Getter.cpp:343, 2114`, `Texture/GL_Texture.cpp:4445`; Espryt
`Managers.cpp:7286, 7391, 7398, 7415`; Magma `VkRenderPassManager.cpp:618`,
`VulkanRenderer.cpp:1806, 1823, 8755, 9243, 10261-10264`.
**The defect class it closes** is visible in Espryt's
`BackendFramebufferObject::SyncReadBufferToBackend` (`Managers.cpp:7410-7430`), whose comment at
`:7424-7426` says it must `Bind(FramebufferTarget::Read)` first "When this is reached from
`SyncCurrentFBO`'s 'same FBO as draw' skip path" — the shared-FBO case where the read-buffer
sync was skipped. With the surface pre-resolved on the client, that whole hazard is
unrepresentable.
- **`Complete`**: `FramebufferObject::CheckCompleteness()` `FramebufferObject.cpp:163-193` — walks
the 41 attachment slots, skips `!IsValid()`, requires equal `GetSize()` and per-attachment
`IsComplete()`, and requires at least one valid attachment. The *GL-visible* status is more
than that: `CheckFramebufferStatus_State` `GL_Framebuffer.cpp:2265-2296` adds
`HasDefinedAttachment` (to pick `INCOMPLETE_ATTACHMENT` vs `MISSING_ATTACHMENT` `:2284-2286`),
`HasNonRenderableColorAttachment` → `UNSUPPORTED` `:2288-2290`, and
`ActiveBackendRejectsDistinctDepthStencil() && HasUnsupportedDistinctDepthStencilAttachments`
`:2291-2294` (**this one reads the backend**, so it cannot be part of a purely-client answer —
see the risk note in §14). `CheckNamedFramebufferStatus_State` `:2298-...` is the same body.
The TODO block at `:2279-2282` records that DRAW_BUFFER / READ_BUFFER / MULTISAMPLE /
LAYER_TARGETS incompleteness is not implemented.
- `Width/Height/Layers/Samples/FixedSampleLocations` for a no-attachment FBO come from
`GetDefaultWidth/Height/Layers/Samples/FixedSampleLocations` (`FramebufferObject.h:141-145`).
- `DrawBuffers[8]` from `GetDrawBuffers()` `:135` (an array of `FramebufferAttachmentType`, so the
client converts to the `Int8` attachment index / `-1` the payload wants, `MGPipeTypes.h:362`).
---
## 8. Texture sub-data accumulation — the union box and the region list already exist
`MG_State/GLState/TextureState/MipmapStorage.h` is where a P4a `MGPSubData` + `MGPSubRegion[]`
is built from; the frontend structure and the payload were designed against each other.
- `struct MipmapDirtyRegion` `:26-39` — `{IntVec3 lo, hi}` half-open, `Empty()` `:29`,
`CoversWholeLevel(levelSize)` `:30-33`, `TexelCount()` `:34-38`. Cleared (all zero) while clean
`:20-25`.
- Union box per level: `m_dirtyRegions` `:119`; `MarkDirtyRegion(level, offset, size)` `:58`
unions and sets the flag; `GetDirtyRegion(level)` `:60` reads it, meaningful only while
`IsDirty(level)` `:54`.
- **Behind it, the disjoint rect list**: `kMaxDirtyRects = 96` `:75`, `m_dirtyRects` `:127`
(per-level `Vector<MipmapDirtyRegion>`), `GetDirtyRects(level, out, maxRects)` `:82`,
`InsertDirtyRect` `:114`. The design notes `:62-81` are load-bearing for the wire format:
the union box "stays the source of truth (every write funnels through
`MarkDirty`/`MarkDirtyRegion` into BOTH representations), backends OPT IN to the list purely as
an upload-size refinement"; **`GetDirtyRects` returning 0 means "upload the union box instead"
and covers every reason at once** — tracking unavailable, a single rect, more rects than
`maxRects`, or a summed area close enough to the box's that one big upload wins `:76-81`.
96 slots because a 16-slot list forced far-apart merges to >90% of the box's area on
Minecraft's ~100 sprites/frame `:68-74`. An EMPTY list is the resting state and always means
"the union box is the whole story" `:120-126`.
- Other level state the descriptor and the blob need: `m_texelSizes` `:116`, `m_data` `:117`
(the level shadow), `m_isDirty` `:118`, `m_compressedData`/`m_compressedFormats` `:128-129`
(kept **beside**, not instead of, the uncompressed shadow — rationale `:84-92`),
`m_requestedCompressedFormats` `:130` (`:99-109`).
- The `ITextureObject` face of all of this: `MarkStorageDirty` `TextureObject.h:252`,
`MarkStorageDirtyRegion` `:257-262` (**base fallback degrades to whole-level**),
`GetStorageDirtyRegion` `:264-267` (**base fallback returns the whole level**),
`GetStorageDirtyRects` `:274-281` (**base fallback returns 0**), `IsStorageDirty` `:253`,
`MapMipmapData` `:251`, `GetMipmapTexelSize` `:243`, `GetMipmapByteSize` `:244`,
`GetMipmapLevelCount` `:242`. Concrete impls
`TextureObject.cpp:379-410` (`TextureObjectWithOneMipmap`), cube overrides
`TextureObject2DCube.cpp:53, 65`.
- The payload side: `MGPSubRegion` `MGPipeTypes.h:607-614` (`X,Y,Z,W,H,D,SrcOffset,SrcRowStride,
SrcSliceStride`; **strides CARRIED, not inferred from a pointer comparison** `:603-606`),
`MGPSubData` `:636-648` (`Res, Target, Level, SourceIsVerbatimLevelShadow, UnionBox,
RegionCount, Blob`). "Carries the union box AND the region list so the SERVER picks the upload
shape — the decision belongs on the side that pays the GPU cost. Mali prices texture upload by
JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame" `:616-618`.
The blob rule `:624-635`: a non-zero `Blob.Size` that is not exactly the record's own byte size
is `Fatal{ProtocolCorruption}`; **zero means "not declared"**, which is what a monolith emission
is. `MGPSubDataComplete` `:686-691` is the forward terminator for a server-initiated pull.
- Espryt's existing consumer, whose shape `MGPSubRegion` copies: `UnpackStagingBlock`
`Managers.cpp:4934-4942` (`src, rowBytes, rows, slices, srcRowStride, srcSliceStride, offset`),
with the ring-staging contract at `:4920-4933` and `:6297`.
---
## 9. Program artifacts, reflection, per-stage SPIR-V, the link version
`MG_State/GLState/ProgramState/ProgramArtifacts.h` (35 KB, 576 lines) is the P0.5 extraction that
makes `MGPProgramDesc`'s reflection blob possible — it includes only `<Includes.h>` (`:10`), never
`ShaderObject.h` or `SpvcSession.h`.
- `kInvalidUniformOffset = ~0u` `:26`.
- `struct TypeFacts` `:31-59` — 20 flattened predicates (13 `Bool` + 7 4-byte scalars),
**POD of exactly 44 bytes**, asserted `:551-552`.
- `struct ResourceReflection` `:63-86` — one flattened `glslang::TObjectReflection`
(`name, glDefineType, offset, size, index, counterIndex, arrayStride, topLevelArraySize,
topLevelArrayStride, binding, location, stages, arraySize, TypeFacts type`); aliased as
`UniformReflection`/`BlockReflection`/`PipeInputReflection`/`PipeOutputReflection` `:88-91`.
- `struct XfbVarying` `:95-120` — `{name, type, size, bufferIndex, offsetBytes, byteSize,
packedOffsetBytes, blockInstanceName, blockName, blockMemberIndex, blockMemberElement}`.
- `struct LinkArtifacts` `:160-343` — the whole link output in one movable block; `program`
(`SharedPtr<glslang::TProgram>`) is **live only between LinkProgram() and the end of
DoReflection** and is null for an L1-memo-served link `:161-165`; then the four owned
reflection vectors `:168-171`, `lastStageIsFragment` `:177`, `computeLocalSize` `:178`, …,
ending at `xfbVaryingNameMaxLength` / `xfbNeedsScatteredCapture` / `xfbPackedStride` `:340-342`.
Indexed by **TProgram index**, deliberately `:155-158`.
- `struct SpirvArtifacts` `:359-399` — `generatedSpirv` (`Vector<Vector<unsigned>>`, **one module
per entry of the linked-shader snapshot, in that order**) `:360`, `enableSpirvValidation` `:361`,
`uniformOffsets` `:364`, `globalUboScratch` `:365`, `reservedNumSamplesOffset` `:370`,
`spirvStatus` `:377`, `nativeFloat64` `:386`, `pointSizeDemoted` `:398`.
- **The archive**: `VisitFields(Self&, Visitor)` free templates `:401-547`, one per type, the same
table serving serializer (const) and deserializer (non-const) `:403-406`. This is the
ready-made writer for `MGPProgramDesc::Reflection`.
- **Trip wires** `:549-575`: `sizeof(TypeFacts)==44`; per-STL pins under `__GLIBCXX__`
`MGL_RESOURCEREFLECTION_SIZE 128`, `MGL_XFBVARYING_SIZE 128`, `MGL_LINKARTIFACTS_SIZE 1056`,
`MGL_SPIRVARTIFACTS_SIZE 88` `:558-562`; the libc++ (NDK) branch is **inert until the integrator
pins it** `:563-565` — that pin is a P4a chore if the archive crosses on device.
`MGPProgramDesc` (`MG_Pipe/MGPipeTypes.h:323-335`, 192 B): `{Cso, StageMask (== the linked stage
set), GlobalUboSize, ReservedNumSamplesOffset, SpirvStatus, NativeFloat64, PointSizeDemoted,
EnableSpirvValidation, MGPBlobRef Spirv[6], MGPBlobRef Reflection}`.
### 9.1 The "as last linked" rule the emitter must obey
`ProgramObject::GetLinkedShaderSnapshot()` `ProgramObject.h:106` returns
`Vector<LinkedShaderRef>` (`:99-103`, `{shader, source, node}`), rebuilt in Link()'s prologue.
`HasLinkedShaderStage(stage)` `:115-120`. **The header states it twice** (`:92-98`, `:121-133`):
`glAttachShader`/`glCompileShader` take effect only at the next link and neither moves
`m_linkVersion`, so **anything keyed on the link generation must consume this snapshot, never the
live attach list** (`GetAttachedShaders()` `:89-90`), and the SPIR-V module list is sized and
indexed by the snapshot, not by the attach list. `StageMask` therefore comes from the snapshot.
### 9.2 The join gate
`Artifacts()`/`Spirv()` join through `EnsureLinkJoined()` (inline `ProgramObject.h:1275`, blocking
half `ProgramObject.cpp:50-83`) and `EnsureSpirvJoined()` (`ProgramObject.cpp:89-122`).
`JoinPendingLink` moves the node out FIRST to avoid re-entry `:55-60`, moves the artifacts
`:66`, and calls `BumpLinkObservableVersions()` `:72` (the *second* bump; the first was at
enqueue `:67-71`). `GLContext::GetProgramForDraw` `Core.cpp:609-628` is join site J1 and calls
`currentProgram->JoinLinkAndSpirv()` `:627` — **both phases, and the comment `:622-626` explains
why phase A alone reopens the hazard**. `GetProgramForDispatch` `Core.cpp:763-...`,
`GetProgramForUniform` `:786-...`.
**P4a's emitter must be the joiner.** ARCHITECTURE.md:198 says `create_shader_state` is emitted
"从编译池的终止 continuation" — i.e. from the compile pool's terminal continuation, not from the
draw — which is the monolith-unreachable async win. That is a design decision for the brief, not
a fact about this tree: today nothing emits it at all.
### 9.3 What the server still specialises (do NOT try to push it)
`docs/Disaggregated/ARCHITECTURE.md:263` lists the eight extra inputs a backend program depends on
(draw-FBO snorm/unorm clamp mask, fragColor broadcast count, storage-block binding signature,
atomic-counter set, live image formats, patch params; Magma adds default-FB height for FragCoord
Y-flip and the XFB layout): `create_shader_state` publishes the **artifacts**, the server
specialises at verb time.
### 9.4 Program pipelines and the composite band
`GLContext::GetProgramForDraw` `Core.cpp:630-...`: when no program is current it takes the bound
pipeline `:631`, **joins every graphics stage program** `:642-645`, computes
`pipeline->ComputeDrawProgramSignature()` `:647` (`ProgramPipelineObject.h:94-98`, an
`Array<Uint64, kGraphicsStageCount*2>` of per-stage lifetimeId+linkVersion), checks
`GetCachedDrawProgram(signature)` `:648-651` (`ProgramPipelineObject.h:149`) and otherwise
**flattens the pipeline into a fresh `MakeShared<ProgramObject>(0u)`** `:661` — deliberately
unnamed (`:658-660`), graphics stages only (`:663-668`: a composite carrying a compute module
SIGSEGVs inside `vkCreateGraphicsPipelines` on Adreno 830).
`RefreshCompositeUniforms` `:649` / `SetMirroredUniformVersions` `:606`
(`ProgramPipelineObject.h:123-148`).
**A composite is an ordinary `ProgramObject` with its own lifetime id**, so it fits the ShaderCso
slot space — and `MGPipeHandles.h:83-97` already reserves the top 1/16 of that space for exactly
these (`kMGPipeShaderCsoSlotLimit = 1<<20` `:88`, `kMGPipeShaderCsoCompositeSlotBase` `:89-90`,
`MGPipeIsCompositeShaderSlot` `:92-94`), and `SlotAllocator.cpp:19-23` refuses the band from the
ordinary allocator. `ProgramPipelineObject` itself has **no lifetime id and no wire object**
(`:19-170`; the only identity is `m_everBound` `:170` / `GetEverBound` `:51`).
`ShaderObject` (`ProgramState/ShaderObject.h:24`, dtor `:52`) has **no lifetime id** — consistent
with `kUnpublishedDestroy` and with the design (shaders never cross).
---
## 10. `PipeInputs` — what already exists for these kinds, and what fills it
`MG_Backend/MGPipe/PipeInputs.h` (736 lines). Storage list
`MGP_INPUT_STORAGE_LIST` `:73-129`; the block `struct PipeInputs` `:158-687`; three storage
classes V/O/F documented `:144-153`; `sizeof(PipeInputs) < 20 KB` asserted `:714`; the
field-count identity `:709-712`.
**P4a-relevant accessors and their storage:**
| accessor | decl | storage member | class |
|---|---|---|---|
| `GetFramebufferBindingSlot(FramebufferTarget)` | `:508-517` | `m_framebufferBindingSlot[kFramebufferTargetCount]` `:681` | O (raw ptr into the live context) |
| `GetImageTextureBinding(Int unit)` ×2 | `:518-533` | `m_imageTextureBindingBase` `:682` | O |
| `GetTextureUnitObject(Int unit)` | `:549-...` | `m_textureUnitBase` `:686` | O |
| `GetProgramForDraw()` | `:539-543` | `m_programForDraw` `:684` | O (`SharedPtr`) |
| `GetProgramForDispatch()` | `:534-538` | `m_programForDispatch` `:683` | O |
| `GetTransformFeedbackProgram()` | `:544-548` | `m_transformFeedbackProgram` `:685` | O |
| `GetActiveTextureUnit()` | `:199-203` | `m_activeTextureUnit` `:629` | V |
| `GetMaxTouchedTextureUnit()` | `:313-317` | `m_maxTouchedTextureUnit` `:647` | V |
| `GetTextureBindGeneration()` | `:398-401` | `m_textureBindGeneration` `:662` | V |
| `GetSamplingResolutionGeneration()` | — | `m_samplingResolutionGeneration` `:661` | V |
| `GetTextureContextId()` | `:403-...` | `m_textureContextId` `:663` | V |
| `GetPixelStoreParameters` | — | `m_pixelStore` (`[0]`=pack, `[1]`=unpack) `:103` | V |
| `GetClampReadColor()` | `:244-248` | `m_clampReadColor` `:635` | V |
| **forwarded (F, sticky)** | `:558-577` | none | `GetProgramObject(Uint)` `:570`, `GetTextureObject(Uint)` `:571`, `ValidateProgramName` `:574`, `GetBufferBindingPointCount` `:569`, `HasOpenTransformFeedbackSpan` `:572`, `InvalidateCompileEnv` `:573`, `RecordError` `:577` |
`kMGPipeForwardedFieldCount = 7` `:135`, asserted equal to the sticky count `:136-137`. The F
accessors carry **no** `MGP_INPUT_CHECK` / `MGP_INPUT_VERIFY_READ` and the reason is stated
`:563-568`.
The two friend doors: `MGPipeFillAccess` `:611` (client filler) and `MGPipeApplyAccess` `:618`
(the applier scattering render-state chunks straight into `m_renderState`).
`gPipeInputs` `:706` — **`inline PipeInputs& gPipeInputs = *new PipeInputs();`**, leak-at-exit,
with the full ID-18 rationale at `:692-705` (its O-class members are `SharedPtr`s to FRONTEND
objects; destroying the block at exit runs `~VertexArrayObject` → `~BufferObject` →
`MGPipeApplyResourceDestroy` → the backend's twin tables). **Any P4a `SharedPtr<ProgramObject>` /
`SharedPtr<ITextureObject>` added here inherits that rule verbatim.**
### 10.1 `FillPoints.def` rows that fill them
`MG_Pipe/FillPoints.def`. Verb list `:79-148` (69 verbs, **must be exactly the member set of
`MG_Backend::GLFunctionsTable` in declaration order** `:17-22`, enforced by `gen_pipe.py`);
class list `:152-153` (nine classes); the may-read table `MGP_FILL_FIELD_LIST` `:158-338`.
P4a's fields, by class:
- `GetFramebufferBindingSlot`: `kDraw` `:200`, `kDispatch` `:219`, `kClear` `:232`,
`kBlitOrCopy` `:247`, `kTextureOp` `:291`, `kReadback` `:296`, `kProgramOp` `:328`.
- `GetTextureUnitObject`: `kDraw` `:165`, `kDispatch` `:213`, `kClear` `:240`, `kBlitOrCopy` `:256`,
`kTextureOp` `:278`, `kReadback` `:298`, `kProgramOp` `:329`.
- `GetImageTextureBinding`: `:170, 218, 245, 261, 279, 308, 334`.
- `GetTextureContextId` / `GetTextureBindGeneration` / `GetSamplingResolutionGeneration` /
`GetMaxTouchedTextureUnit`: the same seven classes (`:166-169, 214-217, 241-244, 257-260,
280-283, 304-307, 330-333`).
- `GetActiveTextureUnit`: `kBlitOrCopy` `:255`, `kTextureOp` `:277`, `kReadback` `:297`.
- `GetProgramForDraw`: `kDraw` `:161`. `GetProgramForDispatch`: `kDispatch` `:209`.
- `GetPixelStoreParameters`: `kReadback` ONLY `:294`.
- `GetClampReadColor`: `kReadback` `:299`.
- The eight statically over-approximated rows are **all KEPT** with reasons `:30-70`; three of
them are P4a-adjacent (`kTextureOp/kDispatch + IsCapabilityEnabled` because Magma's
`GenerateMipmap` and `PrepareStorageImageTextures` materialise a queued clear that reads
`GL_FRAMEBUFFER_SRGB` `:43-49`; `kTextureOp + GetBufferBindingPoint` / `GetFramebufferBindingSlot`
for the depth-mipmap shader path `:284-292`). **`P3a STATUS` `:65-70` says the omission sweep
rides the full gl44to46 caselist run and every row stays until it lands** — so P4a must not
retire one on desktop evidence either.
### 10.2 `Coverage.def` rows P4a's calls will claim
`MG_Pipe/Coverage.def`. Accessor→call map `:29-132`; sticky list `:144-151`; delta list `:156-158`;
**emitted list `:198-232`** (what a call now SUPPLIES, so the residual fill loop may skip it).
Existing accessor rows for P4a's kinds: `GetFramebufferBindingSlot → SetFramebufferState` `:84`,
`GetImageTextureBinding → SetShaderImages` `:85`, `GetActiveTextureUnit → SetSamplerViews` `:30`,
`GetMaxTouchedTextureUnit → SetSamplerViews` `:88`, `GetSamplingResolutionGeneration →
SetSamplerViews` `:109`, `GetTextureBindGeneration → SetSamplerViews` `:112`,
`GetTextureContextId → SetSamplerViews` `:113`, `GetTextureObject → SetSamplerViews` `:114`,
`GetTextureUnitObject → SetSamplerViews` `:115`, `GetProgramForDraw → SetDrawProgram` `:100`,
`GetProgramForDispatch → SetDispatchProgram` `:99`, `GetProgramObject → CreateShaderState` `:101`,
`GetPixelStoreParameters → SetPixelPackState` `:94`.
**None of them is in the EMITTED list yet.** The two standing warnings for P4a:
- `GetPixelStoreParameters` is **deliberately absent from EMITTED** `:173-181` — the field is
`m_pixelStore[2]` and `set_pixel_pack_state` carries the pack half only, so a row there would be
a half-truth that neither the poison nor the verify comparator could see. **P4a must not add it.**
- P3a's own row (`GetBoundVertexArray → BindVertexElements` `:202`) is "shape-only" and the note
`:191-197` says the decision of whether an emitted call supplies the WHOLE field lives in
`PipeFill.cpp`'s `EmittedCallSuppliesTheWholeField` (`:1116`), **deliberately rather than
silently by the row's presence**. Same discipline applies to every P4a row.
---
## 11. The payloads P4a fills (all already declared, `MG_Pipe/MGPipeTypes.h`)
`MGPResourceDesc` `:150-176` (88 B) — one create/respecify shape for buffers, **every texture
target** and renderbuffers: `Target` `:152`, `StorageKind` (== `TextureStorageType`) `:153`,
`BindMask` `:157` (the `ELEMENT_ARRAY` bit is the index-mirror switch `:154-156`),
`InternalFormat` "already resolved to an uncompressed fallback by the client" `:158`,
`Width/Height/Depth` `:159`, `ArrayLayers/Levels/Samples` `:160`,
`FixedSampleLocations/Immutable` `:161`, `Usage`/`StorageFlags` `:162-163`,
`HasDefinedContent` `:164`, `ImageBindableHint` `:165`, `GlNameForDiag` (**diagnostics only** —
a GL name is never an identity `:167-170`), `ViewOf` `:172`, `BufferForTexBuffer` `:173`,
`BufOffset/BufSize` with `kMGPipeWholeBuffer = ~0` `:174, 177`.
`MGPipeResourceRespecifyNeedsAck(desc)` `:680-682` — `desc.Immutable != 0` (rationale `:668-679`).
Other P4a payloads: `MGPSamplerDesc` `:283-287` (32 B); `MGPSamplerView` `:293-304` (36 B, view
restrictions only — rationale `:289-292`: an attachment-only / image-only / CopyImageSubData-only
texture has no sampler view to hang params on); `MGPTextureParams` `:307-318` (32 B, per texture
OBJECT, with `ForceResync` `:314` mirroring Espryt's `m_forceTextureParamsResync`);
`MGPSurface` `:343-353`; `MGPFramebufferState` `:355-372`; `MGPSamplerViews` `:440-444`,
`MGPSamplerStates` `:447-451`, `MGPImageView` `:453-462`, `MGPShaderImages` `:464-468` (all
`{Start, Count, ContentHash}` var-tail headers); `MGPProgramDesc` `:323-335`;
`MGPGlobalConstants` `:507-513`; `MGPSubData`/`MGPSubRegion` `:607-648`; `MGPCopyRegion`
`:708-716`; `MGPMipPlan` `:741-745`; `MGPReadbackInfo` `:748-756`; `MGPClear` `:729-739`;
`MGPBlit` `:718-725`.
Catalogue rows (`MG_Pipe/PipeCalls.def`, `MGP_CALL_LIST_DOCUMENTED_COUNT 71` `:75`):
`CreateSamplerState/DeleteSamplerState` `:104-105`, `CreateSamplerView/DeleteSamplerView`
`:106-107`, `CreateShaderState/BindShaderState/DeleteShaderState` `:108-110`,
`SetFramebufferState` `:113`, `SetSamplerViews` `:117`, `BindSamplerStates` `:118`,
`SetShaderImages` `:119`, `SetGlobalConstants` `:122`, `SetDrawProgram`/`SetDispatchProgram`
`:126-127`, `SetTextureParams` `:133`, `ResourceSubData` `:134`, `ResourceSubDataComplete` `:136`,
`ResourceCopyRegion` `:139`, `GenerateMipmap` `:140`, `GetTextureImage` `:141`, `Blit` `:143`,
`Clear` `:144`, `ReadPixels` `:145`. **Record numbering never churns — the wire opcode is the
1-based position, so a new call is APPENDED to its group** `:26-29`.
---
## 12. The client emission machinery P4a mirrors
- **Slot allocator** `MG_Impl/Pipe/SlotAllocator.{h,cpp}` — `Allocate` `cpp:38-81`,
`AllocateFor` `:83-95`, `FindByLifetimeId` `:97-105`, `Acquire` `:107-111`, `Free` `:113-130`
(**stale-gen free is a no-op** `:117-119`), `IsLive` `:132-137`, `HighWater` `:151-153`,
`Reset` `:161-168`. **Gen moves only on reuse** `cpp:66-75`, `h:24-28`. `MGPipeSlots()`
`cpp:170-181` is **never destroyed** with the ID-18 rationale spelled out at `:171-179`.
- **CSO cache** `MG_Impl/Pipe/CsoCache.h` — the exact template for a P4a sampler-CSO or
program-CSO cache: version-first skip `:14-25`, hash + **memcmp confirm** `:84-98`
(a bare 64-bit equality would alias two states — `:20-24`), `Mint` `:138-172`, LRU `Evict`
`:174-183` (emits `delete_*` and frees the slot), capacity 64 `:53`, counters `:57-69`,
`s_hashForTest` seam `:121-128`, `MGPipeCsoCacheInstance()` never destroyed `:197-203`.
- **Set-hash suppressor** `MG_Impl/Pipe/SetHashSuppressor.h` — `MGPipeSuppressorSlot` `:44-53`
already reserves `SetSamplerViews` (P3b), `BindSamplerStates` (P3b), `SetShaderImages` (P4b),
`SetShaderBuffers` (P4b), `SetStreamOutputTargets` (P4b). **There is no
`SetFramebufferState` slot** — P4a adds one (or latches `MGPFramebufferState::ContentHash`
itself). `ShouldEmit` `:61-67` (**hash 0 is reserved for "never emitted"; a computed 0 remaps to
1** `:31-33`), `Invalidate`/`InvalidateAll` `:71-75`. The requirement a wired slot puts on its
hash is stated `:23-29`: it must cover **every** input the record carries, not just the set.
- **Resource tracker** `MG_Impl/Pipe/ResourceTracker.h` — `MGPipeBindMaskForBufferTarget`
`:96-134` + the exhaustiveness `static_assert` `:136-148`, `MGPipeBuildResourceDesc` `:171-...`,
`MGPipeBuildSubDataRecord` `:225-...`, **`MGPipeForEachSubDataRecordRange` `:257-...` (the
splitter for ranges past a single record's bound)**, `class MGPipeResourceTracker` `:282-...`
(`Acquire` `:292`, `Find` `:303`, `Resolve` `:313`, `Retire` `:325`, `NotePublished` `:342`,
`BindMask` `:355`, `NoteBoundAs` `:369`, `RefreshBindMask` `:407`, counters `:448-463`,
`ResetForTest` `:493`), `MGPipeResourceTrackerInstance()` never destroyed `:554-559`, and the
reverse-channel callbacks `MGPipeClientOnBufferWriteback` `:570-...`,
`MGPipeClientOnGpuWritten` `:596-...`, `MGPipeInstallClientResourceCallbacks` `:623-...`.
- **Vertex-input emitter** `MG_Impl/Pipe/VertexInputEmit.h` — the CSO-with-blob template:
wire builders `:78-104`, `:106-121`, content hash `:123-129`,
`class MGPipeVertexInputEmitter` `:136-...` (`EmitVertexElements` `:157`, `EmitVertexBuffers`
`:200`, `EmitIndexBuffer` `:256`, `RecordIsPublished` `:307`, `NoteRecordDestroyed` `:319`,
`Reset` `:340`, `EmitCreate` `:373`, the blob staging array `:413`), instance never destroyed
`:433-441`.
- **The validate point** `MG_Impl/Pipe/PipeFill.cpp:1504-1620+`: poison serial `:1518-1524`,
verb + identity `:1525-1527`, the no-context early exit that still clears the pending base
instance `:1528-1536`, the dirty walk `:1544-1545`, the `wants()` lambda that goes through
**`MGPipeSubsystemForDirty` and never names a subsystem constant** `:1548-1557`, the
`FreshlyPrimed()` block `:1570-1585` (CSO cache reset, `MGPipeApplierReset`, suppressor
invalidate-all, vertex-input emitter reset — **and the note at `:1574-1582` that the resource
tracker is deliberately NOT reset**), then the per-subsystem emissions `:1587-1615`.
P4a's emitters go in this ladder.
Also here: `SubsystemForEmitter` `:957-...` + its pairing `static_assert`s `:984-1032`,
`kMGPipeWiredSubsystems` `:1071`, `EmittedCallSuppliesTheWholeField` `:1116`,
`MGPipeNoteAggregate` `:529-552`, and the seven F-class forwarder bodies `:875-906`.
- **Applier** `MG_Pipe/PipeApply.h` — `MGPipeResourceOps` `:74-86` (+`Set/Get` `:90-91`),
slot bounds `kMGPipeMaxResourceSlots = 1<<20` / `kMGPipeMaxVertexElementsSlots = 1<<16`
`:112-113` with the "a slot at or above these is `Fatal{ProtocolCorruption}`, never a resize"
rule `:97-111`, `MGPipeResourceRecord` `:117-132`, `MGPipeVertexElementsRecord` `:141-152`,
`MGPipeApplierState` `:154-...` — **and the explicit split at `:194-218`: object records
(`Resources` `:217`, `VertexElementsCsos` `:218`) survive a make-current and are cleared only by
the object's own death signal or `MGPipeApplierReleaseObjectRecords` `:300`; working state
(`BoundVertexElements` `:236`, `VertexBuffers` `:240`, …) is cleared by `MGPipeApplierReset`
`:291`**. Refusal counters `:228-229` exist because "a no-op nobody can see is a dropped call
nobody can see" `:220-227`.
---
## 13. Unit tests under `MG_Test` that cover these frontend objects
All registered with `LABELS unit` via `gtest_discover_tests` (e.g.
`MG_Test/Framebuffer/CMakeLists.txt:20`, `MG_Test/Texture/CMakeLists.txt:20, 38`).
| file | size | suites | what it pins for P4a |
|---|---|---|---|
| `MG_Test/Framebuffer/FramebufferTest.cpp` | 1669 L, **61 `TEST`** | `FramebufferTest`, `PackedReadbackEncodeTest` | attach/detach, draw/read buffer validation, completeness |
| `MG_Test/Texture/TextureTest.cpp` | 6714 L, **192 `TEST`** | `TextureTest`, `SharedExponentRGB9E5Test` | the whole texture surface; **ID-8 records that P3a's wave1/wave2 both appended at the same point here and collided — resolve by union, keep every name** |
| `MG_Test/Texture/TextureViewTest.cpp` | 551 L | `TextureViewTest` (`:147, 159, 169, 178, 198, 224, 236, 259`, …) | view state, `TEXTURE_VIEW_*`, composition onto the root, "a framebuffer attach is bounded by the view's own level count" `:198` — directly `MGPResourceDesc::ViewOf` / `MGPSamplerView` |
| `MG_Test/Texture/VkClearManagerTest.cpp` | | | the queued-clear materialisation `FillPoints.def:284-289` names |
| `MG_Test/State/ObjectLifetimeIdTest.cpp` | 158 L, 6 `TEST(ObjectLifetimeIdTest,…)` | | **the id invariant P4a's handles rest on**: constructs/destroys on the heap 64× (`:57-58`), publishes through a volatile sink so the new/delete pair cannot be elided (`:42-46`), asserts id ≠ 0 (`:72-73`) and strictly increasing (`:74-76`), and **skips rather than passes quietly if the allocator never repeats an address** (`:21-24`). Covers `BufferObject`, `RenderbufferObject`, `VertexArrayObject` (`:34-36`) — **not** Texture/Framebuffer/Sampler/Program, all of which now have ids. Cheapest P4a test win. |
| `MG_Test/State/RenderStateTest.cpp` | | | render state incl. the pixel-store setters |
| `MG_Test/Program/ProgramTest.cpp` | 4990 L | | link, reflection, uniforms |
| `MG_Test/Program/ProgramArtifactsTest.cpp` | 133 L, 5 `TEST(ProgramArtifacts,…)` | `AliasesAreTheSameTypes` `:28`, `TypeFactsIsPodOf44Bytes` `:44`, `VisitFieldsCoversEveryMember` `:80`, `VisitFieldsPassesConstnessThrough` `:90`, `SizesArePinnedOnThisToolchain` `:115` | **the archive contract `MGPProgramDesc::Reflection` serialises through**; `:115` is where the libc++/NDK numbers get read from (`ProgramArtifacts.h:553-557`) |
| `MG_Test/Program/ProgramPipelineCompositeTest.cpp` | 724 L | `ProgramPipelineCompositeTest` | the composite path of §9.4 |
| `MG_Test/Program/{AsyncCompile,AsyncLink,AsyncSpirvPhase,AsyncTeardown,OptimisticStatus,ParallelShaderCompile}Test.cpp` | | | the join gates a P4a emitter will trip |
| `MG_Test/Program/ProgramInterfaceTest.cpp`, `XfbBlockVaryingTest.cpp`, `TessellationLinkTest.cpp` | | | reflection surface |
| `MG_Test/Pipe/*` | | `PipeCatalogueTest` (585 L — payload sizes and the 71-record arithmetic), `PipeInputsTest` (644 L), `TrackerTest` (917 L), `CsoCacheTest` (200 L), `SlotAllocatorTest` (292 L), `ResourceEmitTest` (1874 L), `VertexInputEmitTest` (635 L), `RenderStateSpansTest` (1368 L), `MagmaPipeIdentityTest` (217 L) | **`ResourceEmitTest` and `VertexInputEmitTest` are the two files a P4a `*EmitTest` is modelled on**; `PipeCatalogueTest` is where every new payload size assertion lands |
| `MG_Test/Util/PipeStatsTest.cpp` | | `CounterNamesAreStable` (`:260`), `SummaryLineCarriesEveryClassAndGate` (`:137`) | any new `PipeStats::CallClass` must be pinned in both |
| `MG_Test/ScopedPipeVerb.h` | | | the fixture helper for driving a verb in a unit test |
Fixture trap to carry forward — **ID-14**: `MG_Test/Buffer/BufferTest.cpp`'s `ScopedBackendOps`
scoped only `BufferBackendOps`, so under a push build 26 of 86 dispatch cases routed into the
pipe; `83302ca2` made it save/null/restore the pipe table too, the way `ResourceEmitTest`'s
`ApplierGuard` scopes the applier. **Any P4a test that installs a backend ops table needs the same
two-scope shape**, and the follow-up (a pipe-shaped mock so the same assertions run over the
handle path) is still open.
---
## 14. Named risks and open questions for the brief
1. **`Complete` cannot be a purely client-side answer today.**
`CheckFramebufferStatus_State` (`GL_Framebuffer.cpp:2291-2294`) consults
`ActiveBackendRejectsDistinctDepthStencil()`, and `HasNonRenderableColorAttachment` `:2288`
reads the backend's probed format-capability cache (the comment at `:101` says so).
`FramebufferObject::CheckCompleteness()` (`FramebufferObject.cpp:163-193`) is backend-free but
is a **weaker** answer. Decide explicitly which one `MGPFramebufferState::Complete` carries and
say so in the payload comment; a client that emits the GL-visible status is reading the backend
from the client side, which P4a is supposed to be removing.
2. **Renderbuffer respecify publishes nothing** (§5.2) — a real correctness hole, not a
narrowing item.
3. **`glUseProgram` has no `DirtySurface.def` row** (§5.1); the gate is likely to go red the
moment P4a touches that path.
4. **Espryt's unpack shadow is never invalidated** (§6.2, `Managers.cpp:4911` vs
`DirectGLES.cpp:10578`/`:11112`) — once P4a's descriptor carries the strides, the shadow stops
being a benign optimisation.
5. **`FramebufferObject::SetDrawBuffer` versions the wrong attachment slot**
(`FramebufferObject.cpp:195-199`): it bumps `m_attachmentVersions[buffer]`, i.e. the value being
written, not the index being written to. `m_objectVersion` and the aggregate still move, so bit
11 is safe; a *narrower* P4a shutter built on `m_attachmentVersions` would not be.
6. **The composite band's bookkeeping is unwritten.** `MGPipeHandles.h:83-94` and
`SlotAllocator.cpp:19-23` reserve and defend the band, but nothing mints from it yet, and a
composite is created on a cache miss inside `GetProgramForDraw` (`Core.cpp:661`) — a
`MakeShared` on the draw path, whose death is an ordinary `~ProgramObject`.
7. **`ProgramArtifacts.h`'s libc++ size pins are inert** (`:563-565`). If the archive is to cross
on device in P4a, the integrator owes those four numbers from an NDK build.
8. **`MGPProgramDesc` is 192 B with six SPIR-V blobs; the ring caps one record at half its
capacity** (`ARCHITECTURE.md` §4: `RingProducer::MaxRecordBytes()`), so `create_shader_state` is
named there as one of the two emissions that must be split by the emitter. `MGPipeForEach
SubDataRecordRange` (`ResourceTracker.h:257`) is the existing splitter shape.
9. **ID-19's cost budget.** P3a already added +699 (Espryt) / +422 (Magma) ns/draw and rd12 moved
+30%/+27% on device. Every P4a per-verb emission needs a suppressor slot or a version-first
skip *before* it hashes anything — the `MGPipeCsoCache::Acquire` shape (`CsoCache.h:14-25`),
not a hash-every-verb shape.
10. **`kUnpublishedDestroy` for pipelines and shaders** (`DirtySurface.def:301-302, 305`) should be
resolved deliberately: a program pipeline has no wire object and a shader never crosses, so the
honest P4a outcome may be "the program row becomes `kExplicitDestroy`, the other two stay
`kUnpublishedDestroy` with an updated reason", not "all three close".