Files
MobileGL/docs/Disaggregated/notes/p4a/scout-espryt-sampler-program.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

811 lines
55 KiB
Markdown

# P4a scout — Espryt's sampler / image / program surface
Tree read: `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg` at
`feat/disaggregated@37da3c3a` (P3a closed, ID-24). Every line number below was opened in that tree.
Paths are repo-relative under `MobileGL/` unless written out in full.
House shape assumed from `~/w7/notes/p3a/BRIEF-P3A.md` §A/§C and `INTEGRATOR-DECISIONS.md` ID-1..24:
a phase is 4-5 packages (contract → wire → client → backend → gates), the contract commit is tagged
and frozen, G1 (pull build symbol-identical, admitted-resize set EMPTY) is the hardest gate, and every
backend edit lives inside `#if MOBILEGL_PIPE_PUSH`.
---
## 0. The one-paragraph orientation
P3a converted **one** family (buffers) to a handle-addressed op table and **one** CSO (vertex
elements) to an applier record. P4a's sampler/program half is structurally *easier* in one way and
*harder* in three:
* **Easier**: the payloads already exist and are already in the verify comparator's list.
`MGPSamplerDesc` (`MG_Pipe/MGPipeTypes.h:283-287`), `MGPSamplerView` (`:293-304`),
`MGPTextureParams` (`:307-318`), `MGPProgramDesc` (`:323-335`), `MGPBoundView`/`MGPSamplerViews`
(`:432-444`), `MGPSamplerStates` (`:447-451`), `MGPImageView`/`MGPShaderImages` (`:453-468`),
`MGPGlobalConstants` (`:507-513`). All twelve are already in `MGP_VERIFY_PAYLOAD_LIST`
(`MG_Pipe/PipeFields.def:314-336`) with their `MGP_FIELDS_*` tables (`:74-88`, `:108-134`).
The catalogue rows exist too and are `kNone`/`kHasBlob`/`kVarTail` today
(`MG_Pipe/PipeCalls.def:104-110`, `:117-119`, `:127`, `:134-135`).
* **Harder 1**: sampler state has **two** carriers in MobileGL (a `SamplerObject` bound to a unit,
and a `SamplerObject` *owned by every texture object*), and Espryt pushes them through **two
different driver entry-point families** (`glSamplerParameter*` vs `glTexParameter*`), with two
independent caches. §1.
* **Harder 2**: the shader CSO's wire form is SPIR-V + the whole `LinkArtifacts`+`SpirvArtifacts`
archive (`ARCHITECTURE.md:259-266`), but the archive **has no serializer yet** — only the field
tables. §3.9.
* **Harder 3**: three of the four things Espryt's program twin is keyed on are **not program state
at all** (draw-FBO clamp masks, fragColor broadcast count, live image-unit formats, patch
parameters). That is the `st_variant`-style lazy specialisation `ARCHITECTURE.md:264` names, and
it is what stops `create_shader_state` from being self-contained. §3.4.
---
## 1. Samplers
### 1.1 `SamplerParameters` and `BorderColorForm` — the value type
`MG_Pipe/MGPipeValueTypes.h:462-486`. 15 members, `sizeof == 100`, trivially copyable
(`:615` static_assert; pinned again in `MG_Test/Pipe/PipeCatalogueTest.cpp:131` and `:138`):
```
wrapS/wrapT/wrapR SamplerWrapMode (enum class, 4 B each) default Repeat
minFilter SamplerFilterMode default Nearest
magFilter SamplerFilterMode default Linear
mipmapMode SamplerMipmapMode default Linear
minLod/maxLod Float -1000 / +1000
lodBias Float 0
maxAnisotropy Float 1
compareFunc SamplerCompareFunc LessEqual (NOT Never - table 23.18)
compareMode SamplerCompareMode None
borderColor FloatVec4 (0,0,0,0)
borderColorI IntVec4
borderColorUI UintVec4
borderColorForm BorderColorForm : Uint8 Float
```
`BorderColorForm` is declared at `:449-460` with the rationale verbatim: all three representations
are *always* numerically populated, so the value alone cannot say which driver entry point to use.
`MGPipeTypes.h:279-282` says the struct "crosses byte for byte INCLUDING borderColorForm".
> **TRAP (P4a must fix in the contract commit).** `SamplerParameters` has **3 bytes of trailing
> padding** (96 bytes of members + 1 byte `borderColorForm` = 97, padded to 100). There is **no
> `MGP_FIELDS_SamplerParameters`** in `PipeFields.def` (grep: only `RenderStateParameters` at `:233`
> and `PixelStoreParameters` at `:253` of the value structs), and `SamplerParameters` is **not** in
> `MGP_VERIFY_PAYLOAD_LIST`. `MGPSamplerDesc::Parameters` is an `MGPBlobRef`, so under
> `MOBILEGL_PIPE_VERIFY` the blob is compared as bytes and the padding false-differs; and any
> content hash taken over those 100 bytes hashes uninitialised padding, which would mint a fresh
> sampler CSO per call. This is exactly the shape the P3a brief's G7 negative control exists for on
> the vertex side. Add the field table + the verify-list row, and hash/compare field-wise.
### 1.2 The `SamplerCso` slot table (P2)
`MG_Backend/DirectGLES/Managers.h:2228-2229`:
```cpp
extern TwinRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject, MG_Pipe::MGPipeKind::SamplerCso>
g_backendSamplerObjects;
```
defined at `Managers.cpp:10583`. `TwinRegistry` is the alias at `Managers.h:1188-1195` that swallows
the kind parameter in the pull build (G1); the push arm holds a
`BackendSlotTable<SamplerObject, BackendSamplerObject, SamplerCso>` member at `Managers.h:538`, and
`StateBackendObjectRegistry::GetOrCreate` dispatches to it at `Managers.h:325-332` when
`EsprytSlotTablesEnabled()` (`SlotTables.h:132-135`).
The handle-keyed overloads P4a needs already exist and are **unused for this kind**:
`BackendSlotTable::GetOrCreate(MGPipeHandle)` (`SlotTables.h:278-326` — forward Gen = recycle+reset,
**backward Gen = refuse**, `kMaxHandleSlot = 1<<20` at `:148`), `FindByHandle` (`:376-382`),
`ReleaseByHandle` (`:350-361`), `LiveGenAt` (`:331-335`).
**Death today is the shared notice, not a call.** `Managers.cpp:199-247`
`OnFrontendStateObjectDestroyed` switches on kind; `MGPipeKind::SamplerCso` →
`SamplerImpl::g_backendSamplerObjects.DestroyByLifetimeId(lifetimeId)` (`Managers.cpp:211-213`),
which forwards to `BackendSlotTable::OnFrontendObjectDestroyed` (`Managers.h:442-446`,
`SlotTables.h:416-430`). The raiser is `SamplerObject`'s push-only destructor
(`MG_State/GLState/SamplerState/SamplerObject.h:18-24` — declared only under `MOBILEGL_PIPE_PUSH`
so the pull build keeps its implicit dtor, G1). The ops table is installed at `Managers.cpp:247-250`
+ `:326`. **P4a decision owed**: does `SamplerCso` follow buffers (its own `DeleteSamplerState` call,
client frees the slot — the `default:` comment at `Managers.cpp:236-246` spells the ordering rule) or
does it keep the shared notice? The catalogue already has `DeleteSamplerState`
(`PipeCalls.def:105`), and the P3a `VertexElementsCso` case (`Managers.cpp:225-236`) is the
precedent for the **hybrid**: the client speaks the whole death *and* the backend keeps a notice
consumer for the driver object, with a documented double-release that is a no-op.
### 1.3 `BackendSamplerObject` — the twin
Declared `Managers.h:2205-2223`, defined `Managers.cpp:10416-10584`.
| member | line | note |
|---|---|---|
| `m_backendSamplerId` | `Managers.h:2218` | `glGenSamplers` in ctor `Managers.cpp:10417-10429` |
| `m_contextGeneration` | `:2219` | vs `g_backendContextGeneration`; dtor refuses to delete a foreign name |
| `m_isInitialized` | `:2220` | |
| `m_cacheSamplerParameters` | `:2221` | the whole 100-byte struct, as the last-pushed shadow |
| `m_syncedSamplerVersion` | `:2222` | `Uint16`, vs `SamplerObject::GetVersion()` |
`SyncToBackend` (`Managers.cpp:10451-10556`): version early-out at `:10461-10465`; then a
per-field diff against `m_cacheSamplerParameters`:
* min-filter + mipmap-mode fused, through `ResolveBackendMinFilter(params, ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe())`
(`Managers.cpp:10482-10490`; the resolver is `Managers.cpp:100-117` and collapses the four
`*_MIPMAP_*` filters to `GL_NEAREST`/`GL_LINEAR` on ANGLE-on-llvmpipe);
* mag filter `:10491-10497`;
* `SYNC_SAMPLER_PARAM_IF_CHANGED` macro (`:10475-10480`) for wrapS/T/R, compareFunc, compareMode
(`:10498-10502`);
* minLod / maxLod `:10503-10510`;
* maxAnisotropy behind `g_GLESCapabilities.SupportsTextureFilterAnisotropy`, cache updated even when
the call is skipped `:10511-10517`;
* **border colour**, `:10518-10553`: gated on `SupportsTextureBorderClamp`, branches on
`borderColorForm` to `glSamplerParameterIiv` / `Iuiv` / `fv`, with the entry-point null checks;
the redundancy filter compares **all four** (float, int, uint, form) `:10518-10521`.
* **`lodBias` is never pushed here.** ES has no `GL_TEXTURE_LOD_BIAS`; it is folded into the shader
as a uniform — see §3.5.
`Bind(unit)` (`:10558-10566`) dedups against `g_boundSamplersCache[unit]` and calls `glBindSampler`.
`UnbindSampler(unit)` (`:10575-10581`) is the symmetric zero-bind. Destructor
(`:10431-10449`) scrubs **every** row of `g_boundSamplersCache` that names `this` before deleting —
the recycled-heap-address defence that `{slot,gen}` is supposed to retire.
### 1.4 The unit shadow
`Array<BackendSamplerObject*, TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundSamplersCache`
— declared `Managers.h:2226-2227`, defined `Managers.cpp:10582`. `MAX_TEXTURE_IMAGE_UNITS` is **192**
(`MGPipeTypes.h:429-431` states it: "one Array of MAX_TEXTURE_IMAGE_UNITS = 192", merged unit space,
no stage dimension — which is why `PipeCalls.def:194-196` deliberately drops the stage dimension of
`set_sampler_views`).
This shadow is a **raw twin pointer per unit** and is read by three memos as a proof-of-driver-state
(§1.5, §1.6). Under a split it cannot be a pointer; it becomes the applier's own per-unit
`MGPipeHandle` array. That is the single largest mechanical change on the sampler side.
### 1.5 Per-unit sampler walk — `BindCurrentUnitSamplers`
`MG_Backend/DirectGLES/DirectGLES.cpp:3593-3626`, called last from `BindCurrentTextures`
(`DirectGLES.cpp:3752`).
Two memos stack here:
1. **`UnitSamplerLookupMemo`** (`DirectGLES.cpp:3522-3535`), one row per unit (192 rows). Push arm
stores `MGPipeHandle frontendHandle` (`:3525`); legacy arm stores a `WeakPtr<SamplerObject>`
under `MOBILEGL_PIPE_LEGACY_MEMOS` (`:3528-3530`). `ResolveUnitSamplerBackend`
(`:3537-3572`): push arm probes `g_backendSamplerObjects.HandleOf(sampler)` then `FindByHandle`
(`:3543-3556`); legacy arm uses `OwnerEquals` + `Find` (`:3558-3568`). **A miss is never cached**
(`:3550-3553`) because the program pass creates the twin later in the same draw.
`SlotTables.h:556-561` records that this caller *thrashes* the table's single-entry
lifetime-id memo (a different sampler per unit) — a known, unpaid perf item.
2. **The walk memo** (`DirectGLES.cpp:3588-3593`): `g_unitSamplerWalkContextId`,
`…Epoch`, `…MaxUnit`, `…ContextGeneration`, `…Valid`, plus
`g_unitSamplerWalkRows` — a **full 192-entry copy of `g_boundSamplersCache`**. The replay gate
(`:3597-3604`) is keys-equal **AND** `memcmp` of the first `maxTouchedUnit+1` rows. The
invalidation argument is written out at `:3575-3587`.
The walk itself (`:3605-3616`): for every unit ≤ `maxTouchedUnit`, take
`MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject()`; bind its twin, or `UnbindSampler(unit)`.
### 1.6 `SamplerPassMemo` — the program-side sampler pass
Declared `Managers.h:1913-1924` (with the six-bullet invalidation enumeration at `:1895-1912`),
member at `Managers.h:2117`, accessor `Managers.h:2001`.
```cpp
struct SamplerPassMemo { // Managers.h:1913
static constexpr SizeT kMaxEntries = 16; // :1914
Bool valid; Uint8 count;
Uint64 contextId, unitBindingsEpoch, samplingGeneration;
Uint32 backendStateVersion; Uint textureContextGeneration;
Array<Uint8, 16> units; // :1922
Array<BackendSamplerObject*, 16> rows; // :1923 - driver-state proof
};
```
Consumed in `BindCurrentProgramWithResources` at `DirectGLES.cpp:3922-4038`: the clean test is the
five keys (`:3936-3941`) plus a per-entry row compare against `g_boundSamplersCache`
(`:3942-3950`). A pass touching >16 units, or an out-of-range unit, simply never memoises
(`:3956-3966`). Reset on relink at `Managers.cpp:9258`.
The pass body (`:3954-4030`) does five things per `SamplerUniformBinding`:
1. resolve the frontend unit — `currentProgram->GetUniformSamplerOrImageUnitIndex(frontendLocation)`
(`:3958-3960`);
2. `glUniform1i(backendLocation, unit)` if it moved (`:3971-3974`);
3. the **LOD-bias uniform** (`:3982-4000`) — sampler-object bias wins, else the *texture's own*
sampler's bias, looked up through `SamplerUniformTextureTarget(uniformType)`
(`DirectGLES.cpp:3553`ff mapping table, i.e. `DirectGLES.cpp` right after
`NeedsRawDepthFetchSampler`);
4. the **raw-depth-fetch substitution** (`:4001-4010`);
5. otherwise `SyncToBackend` + `Bind(unit)` on the unit's sampler object, creating the twin if
`ResolveUnitSamplerBackend` missed (`:4011-4026`), or `UnbindSampler(unit)` (`:4027-4029`).
**This is the only site in Espryt that ever creates a `BackendSamplerObject`**
(`DirectGLES.cpp:4014-4020`) — there is no eager `glGenSamplers` at `glGenSamplers` time.
`create_sampler_state` under P4a inverts that: the CSO is minted and emitted at the frontend call,
and the draw path only binds. Say so in the brief; it changes when `glGenSamplers` runs on the
driver and therefore what `MGLOG_E_ONCE("Failed to generate sampler object.")` at
`Managers.cpp:10424` can report.
### 1.7 The raw-depth-fetch sampler
`DirectGLES.cpp:61-62` (two file-static `SharedPtr`s), `:207-219` `GetRawDepthFetchSampler()`,
`:221-235` `NeedsRawDepthFetchSampler()`.
It **constructs a frontend `MG_State::GLState::SamplerObject(0)`** inside the backend
(`:209-214`) purely to reuse `BackendSamplerObject::SyncToBackend`'s signature — nearest/nearest,
no mipmaps, compare mode off, compare func Always. `ARCHITECTURE.md:322` names it explicitly:
> Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。
so P4a is expected to replace it with a backend-native sampler (a `glGenSamplers` id + a literal
parameter push), not to route it through a CSO. `NeedsRawDepthFetchSampler` reads
`IsDepthFormatInternalFormat(textureFormat)` and three sampler params (compareMode, minFilter,
mipmapMode, magFilter) — under a split those come from the **resolved sampler view + the sampler
CSO's parameters**, both of which the applier will hold. `ARCHITECTURE.md:206` already assigns this
post-processing to the **server**, acting on the already-resolved set:
> 两处后端特定后处理留在 server、作用于已解析集合:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。
### 1.8 Texture-object sampling state — the *second* carrier
Every `ITextureObject` owns a `SamplerObject` (`GetSamplerObject()`), and Espryt pushes it with
`glTexParameter*` in `BackendTextureObject::SyncBuiltinSamplerToBackend`
(`Managers.cpp:6671-6781`):
* version gate on `samplerObject->GetVersion()` **plus** `m_forceSamplerResync`
(`:6683-6690`; the flag's rationale is `Managers.h:1508-1519` — a driver re-mint loses every
pushed parameter, and an incomplete texture samples (0,0,0,1));
* multisample targets take **no** parameters at all — the cache is stamped and the function returns
(`:6706-6710`);
* min/mag filter through the same `ResolveBackendMinFilter`, but with `IsAngleLlvmpipeRenderer()`
rather than the sampler-object predicate (`:6732-6744`) — **an asymmetry to preserve**;
* `SYNC_TEX_SAMPLER_PARAM_IF_CHANGED` (`:6719-6731`) for wrapS/T, wrapR **only when
`SupportsWrapR(targetInternal)`** (`:6752-6757`), compareFunc, compareMode;
* minLod/maxLod/maxAnisotropy `:6758-6776`;
* **no border colour here.** The texture's border colour is pushed from
`SyncTextureParamsToBackend` instead (`Managers.cpp:6911-6954`), reading
`stateTextureObject->GetBorderColorForm()/GetBorderColor()/…I()/…UI()` and comparing against four
separate members `m_cacheBorderColor{,I,UI,Form}` (`Managers.h:1488-1493`). That split matters:
the border colour is *sampler* state per GL 4.6 table 23.18 (`MGPipeValueTypes.h:478-483`) but
Espryt pushes it on the **texture params** version, not the sampler version.
So a P4a `MGPSamplerDesc` covers the *sampler object* half; the texture's own sampling state is
`set_texture_params` (`MGPTextureParams`, `MGPipeTypes.h:307-318`) **plus** a second sampler CSO or
an inline parameter block — and today `MGPTextureParams` carries `BaseLevel/MaxLevel/Swizzle/
DepthStencilMode/ForceResync/MinLod/MaxLod/LodBias` and **no filter, no wrap, no border colour, no
compare mode**. That is a real gap between the payload and what
`SyncBuiltinSamplerToBackend` + the border block actually push. Resolve it in the contract commit —
either widen `MGPTextureParams` (it is 32 B today, `MGP_ASSERT_POD` at `:318`) or give every
texture object an implicit sampler CSO.
### 1.9 Seamless cube maps — a non-item, state it so nobody looks for it
`GL_TEXTURE_CUBE_MAP_SEAMLESS` is **not** sampler state in this codebase. It is
`RenderStateParameters::TextureCubeMapSeamlessEnabled` (`MGPipeValueTypes.h:305`, packed into the
alignment hole so `sizeof(RenderStateParameters)` stays 1168 — `:292-303`), lives in the **P1
pipeline chunk** (`MG_Pipe/MGPipeRenderStateSpans.h:64-67`), and therefore already crosses as part
of `create_render_state`/`bind_render_state` since P2. `PipeApply.cpp:72` and `:370` carry it in the
capability list. **`grep -rn CubeMapSeamless MG_Backend/` returns nothing** — no backend reads it
(ES 3.0+ cube sampling is unconditionally seamless), and the frontend answers `glIsEnabled` from
`RenderState.cpp:340` / `:420`. Nothing for P4a to do.
### 1.10 Complete list of sampler-state reads from backend code
| site | file:line | reads |
|---|---|---|
| `DirectGLES.cpp:227` | raw-depth probe | `samplerObject->GetAllSamplerParameters()` (compareMode, minFilter, mipmapMode, magFilter) |
| `DirectGLES.cpp:3607` | unit walk | `GetTextureUnitObject(unit).GetSamplerObject()` |
| `DirectGLES.cpp:3986` | LOD bias | `samplerObject->GetLodBias()` |
| `DirectGLES.cpp:3993-3995` | LOD bias fallback | `boundTexture->GetSamplerObject()->GetLodBias()` |
| `DirectGLES.cpp:4004-4008` | raw-depth gate | unit sampler, else `texture2D->GetSamplerObject()`; `texture2D->GetFormat()` |
| `DirectGLES.cpp:3468-3474` | completeness | `textureUnit.GetSamplerObject()` else `textureObject->GetSamplerObject()`, then `MG_State::GLState::SamplesAsIncompleteTexture` (`MG_State/GLState/TextureState/TextureObject.cpp:489-512`) |
| `Managers.cpp:6705` | texture built-in sampler | `samplerObject->GetAllSamplerParameters()` |
| `Managers.cpp:6683` | version gate | `samplerObject->GetVersion()` |
| `Managers.cpp:10473` | sampler object | `stateSamplerObject->GetAllSamplerParameters()` |
| `Managers.cpp:10461`,`:10471`,`:10502` | ids/versions | `GetVersion()`, `GetExternalIndex()` |
| `Managers.cpp:6925`,`:6932`,`:6937`,`:6942` | border | `GetBorderColorForm/I/UI/…` on the **texture object** |
| `Managers.cpp:4838` | reset | `m_cacheSamplerParameters = SamplerParameters{}` (on backend texture re-mint) |
`DirectVulkan` uses the same helpers (`MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:515`,
`:828`, `:857`, `:1676`) — a reminder that `SamplesAsIncompleteTexture`/`IsUndefinedDefaultTexture`
resolution is shared and, per `ARCHITECTURE.md:206`, moves **client-side** in P4a
("~40 行搬迁"), leaving only the two backend-specific post-processings on the server.
---
## 2. Images (`set_shader_images`' counterpart)
### 2.1 The payload that exists
`MGPImageView` (`MGPipeTypes.h:453-462`): `Res, Unit, InternalFormat, Layer, Level:Uint16,
Layered:Uint8, Access:Uint8` (24 B). `MGPShaderImages` (`:464-468`): `Start, Count, ContentHash`
var-tail header. Fields tables `PipeFields.def:117-121`.
Catalogue row `PipeCalls.def:119` — `kCtxState, kVarTail`.
Coverage row `Coverage.def:85` maps `GetImageTextureBinding → SetShaderImages`.
Dirty bit `MGPipeDirty::NewShaderImages` (`MG_Impl/Pipe/Tracker.h:79`), computed at
`Tracker.h:311-312` as `mix(mix(textureContent, textureParams), programImageUnitVersion)` — already
live, just not emitted (`kMGPipeDirtyEmittedAtP3a` at `Tracker.h:104-107` stops at bit 10).
### 2.2 The single funnel: `SyncImageTextureBinding`
`DirectGLES.cpp:2057-2135`. Everything about an image unit passes through here — the file says so at
`:2019-2021` ("`glBindImageTextures` is a frontend loop over `glBindImageTexture`, and the whole-sweep
`SyncImageTextureBindings` goes through it too"). It:
* maintains `g_writableImageBufferUnits[192]` + `g_writableImageBufferUnitCount`
(`:2029-2031`, tracker `:2038-2054`, predicate `:2033-2036`);
* maintains `g_imageUnitHighWaterMark` (`:2055`, bumped `:2063-2065`);
* unbinds with `glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8)` `:2066-2069`;
* calls `SyncTextureObjectToBackend(texture, /*imageBindableStorageRequired=*/true)` `:2071` —
**this is the call that forces immutable storage and can re-mint the ES texture name**;
* normalises `layered`/`layer` through `SupportsLayeredImageBinding` (`:1992-2013`, and note the
rule: it asks the **backend** target after `MapToBackendTextureTarget`, and `layer` is forced to 0
for a non-layerable target — Adreno took a stray layer index literally);
* rewrites the bind **format**: buffer textures take the *split* view
(`GetImageBindableBufferSplitFormat`, `backendTexture->GetBufferImageSplitViewId()`,
`:2126-2138`), everything else takes the *widening*
(`GetImageBindableStorageWidening`, `:2139-2145`);
* finally `glBindImageTexture(unit, bindTextureId, Level, layered, layer, Access, bindFormat)` `:2133`.
`MarkWritableImageBufferTexturesGpuWritten` (`DirectGLES.cpp:2147-2171`) is the GPU-write
announcement for buffer textures on writable image units; under push it already routes through
`BufferImpl::MarkBufferGpuWritten` (`:2166-2169`), i.e. the P3a reverse channel.
`SyncImageTextureBindings` (`:2173-2185`) sweeps `min(192, MaxImageUnits)` units.
### 2.3 The image sweep memo
`DirectGLES.cpp:2190-2193` (`g_imageSweepContextId`, `g_imageSweepSamplingGeneration`,
`g_imageSweepBackendContextGeneration`, `g_imageSweepValid`), gate
`SyncImageTextureBindingsForDraw` at `:2202-2214`. Two properties P4a must keep:
* the `g_imageUnitHighWaterMark == 0` early-out (`:2203`) is what makes every Minecraft draw pay one
integer test;
* the gate key is deliberately the **frontend** sampling-resolution generation and **not** a
backend re-mint counter, because a texture bound *only* to an image unit is re-minted *inside* the
sweep (`:2195-2201`). A server-side epoch would be bumped after the gate had already declined.
Called from the draw path at `DirectGLES.cpp:3355`.
### 2.4 The photon canary — image binding semantics
Memory `flywheel-indirect-lessons` item 5 (and item 2) applies directly, and its mechanism is in
this tree:
* `RebindImageUniformsToFrontendUnits` (`Managers.cpp:602-651`) rewrites every image uniform
declaration's `layout(binding = N)` to the **frontend-tracked unit**
(`GetUniformLocation` → `GetUniformSamplerOrImageUnitIndex`, `:627-635`), because glslang
auto-assigns a binding when desktop GL source omits one while the app picks the unit with
`glUniform1i` (illegal on ES image uniforms). Called at `Managers.cpp:9800`, and the ordering
constraints against `BakeImageFormatQualifiers` / `RemapImageArrayElementUnits` /
`SplitReadWriteImageUniforms` / `RemoveLayoutBinding` are spelled out at `:9801-9851`.
* `IsImageUniformType` (`Managers.h:1826-1867`) is the **33-entry** contiguous block
`GL_IMAGE_1D`..`GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY`; the header records that the earlier
15-entry list caused two bugs (a non-baked non-core format losing the stage, and an image uniform
assigned with `glUniform1i`).
* `CacheResourceLocations` skips image uniforms entirely (`Managers.cpp:10316-10321`).
**The canary itself** (from the memory note, still the rule): real-device Flywheel testing does NOT
cover Iris packs; `photon-v1.3b` on llvmpipe retrace is what catches an image/SSBO binding-semantics
regression. `MEASUREMENTS`/roadmap caveat: photon is broken on Adreno (memory
`photon-broken-on-adreno`) — run it on llvmpipe, not on the phone. P4a moves the image-unit
resolution to `set_shader_images` records and the *format bake* stays server-side (the format the
shader was built against is live `glBindImageTexture` state, §3.4), so this is precisely the change
the canary exists for. **Recommend the brief pin `photon-v1.3b` desktop retrace as a named G3b
case for P4a.**
### 2.5 Format-less image bake (the fourth staleness key)
`ImageFormatBakeInputs` (`Managers.h:2170-2199`) + `CollectImageFormatBakeInputs`
(`Managers.cpp:8327-8390`): walks the program's uniform reflection
(`GetMaxUniformLocation`/`GetUniformName`/`GetUniformType`/`GetUniformTypeFacts`, `:8346-8351`),
resolves each format-less image's unit (`GetUniformSamplerOrImageUnitIndex`, `:8377`), reads the
**live** bound format (`BoundImageUnitFormat`, `Managers.cpp:8155-8161`) and mixes a signature
(`MixImageUnitFormat`, `:8163`). Stored on the twin as `m_formatlessImageUnits` +
`m_imageUnitFormatSignature` (`Managers.h:2118-2119`, written `Managers.cpp:9287-9288`) and
re-checked per draw by `ImageUnitFormatsStillMatch()` (`Managers.cpp:8544-8547`, declared
`Managers.h:2030`), consumed in the rebuild condition at `DirectGLES.cpp:3164`.
`FormatlessImageBakeScenario` and `NonCoreImageFormatScenario` are its itests.
---
## 3. Programs
### 3.1 The `ShaderCso` slot table and the composite band
`Managers.h:2130-2131` / `Managers.cpp:8065`:
`TwinRegistry<ProgramObject, BackendProgramObjectImpl, MGPipeKind::ShaderCso> g_backendProgramObjects`.
Slot space: `MGPipeHandles.h:84-92` reserves the **top 1/16** of the `ShaderCso` space
(`kMGPipeShaderCsoSlotLimit = 1<<20`, `kMGPipeShaderCsoCompositeSlotBase = limit - limit/16`) for
**program-pipeline composites**, with `MGPipeIsCompositeShaderSlot(slot)` at `:90-92`. That band is
allocated and asserted but **nothing mints into it yet** — P4a's composite resolver does.
Resolution today, `PrgramImpl::SyncCurrentProgram` at `DirectGLES.cpp:3104-3128`: push arm
`g_backendProgramObjects.Find` → `GetOrCreate` (`:3106-3113`); legacy arm goes through
`g_programTwinLookupMemo` (`DirectGLES.cpp:134-135`, an 8-entry `TwinLookupMemo` declared at
`DirectGLES.cpp:93`), `:3115-3127`. A per-draw stash `g_currentDrawFrontendProgram` /
`g_currentDrawBackendProgram` is written at `:3202-3203` and read at `DirectGLES.cpp:3775-3778`
and `:4046-4052`.
### 3.2 The rebuild condition — nine clauses, and where each one comes from
`DirectGLES.cpp:3148-3200`. `twin->SyncToBackend(currentProgram)` runs when **any** of:
| clause | line | source of truth |
|---|---|---|
| no backend program id | 3148 | |
| `GetSyncedLinkVersion() != GetLinkVersion()` | 3150 | program state |
| `GetSyncedImageUnitVersion() != GetImageUnitVersion()` | 3151 | program state |
| snorm clamp mask ≠ `g_snormFallbackClampOutputMask` | 3152 | **draw FBO** |
| unorm clamp mask ≠ `g_unormFallbackClampOutputMask` | 3153 | **draw FBO** |
| `GetFragColorBroadcastCount() != g_fragColorBroadcastCount` | 3154 | **draw FBO**, memoised at `:3078-3101` |
| SSBO binding signature ≠ `ComputeShaderStorageBlockBindingSignature(*program)` | 3155-3156 | program state |
| `!ImageUnitFormatsStillMatch()` | 3164 | **live image-unit state** |
| patch-vertices/outer/inner mismatch (bitwise compare, NaN-safe — `:3183-3186`) | 3188-3196 | **context state** (`MGB_CTX->GetPatchVertices()` etc.) |
The comment at `:3187-3199` explains why the `gl_PerVertex` member set needs **no** clause on this
backend (it mirrors the neighbouring stages' emitted ESSL) while DirectVulkan does.
`GetSyncedLinkVersion()` call sites: `DirectGLES.cpp:3150`, `:4110`, `:7857`; written once at
`Managers.cpp:10234`; declared `Managers.h:2004`.
### 3.3 The `BackendProgramObjectImpl` surface
Class `Managers.h:1873-2122`. Public accessors that P4a's `MGPProgramDesc`/applier must reproduce
or replace: `GetIndirectParamsBinding` (`:1959`), `GetBackendProgramId` (`:1960`),
`IsBackendProgramUsable` (`:1964`), `GetBackendGlobalUBOId` (`:1965`),
`GetSnormFallbackClampOutputMask`/`GetUnormFallbackClampOutputMask` (`:1966-1967`),
`GetFragColorBroadcastCount` (`:1968`), `GetShaderStorageBlockBindingSignature` (`:1973`),
`GetAtomicCounterBindings` + `GetAtomicCounterEsslBindingTop` (`:1979-1980`),
`GetPassthroughTessControl{PatchVertices,OuterLevel,InnerLevel}` (`:1986-1998`),
`HasGlobalUboBlock` / `GetUniformBlockBackendIndices` / `GetSamplerUniformBindings` (`:1997-1999`),
`GetLastUploadedGlobalUboVersion`/`Set…` (`:2000-2001`), `GetGlobalUboBackendBlockSize` (`:1999`),
`GetGlobalUboRingAllocation` (`:2000`), `GetSamplerPassMemo` (`:2001`),
`ReadsDrawID`/`ReadsBaseVertex`/`RoutesViewportIndex` (`:1946-1958`),
`SetBaseInstance`/`SetBaseInstanceWordIndex`/`SetDrawID`/`SetBaseVertex`/`SetViewportPassMask`
(`:1939-1952`). Private state at `:2066-2121`.
`SamplerUniformBinding` (`Managers.h:1880-1891`): `frontendLocation, backendLocation, uniformType,
lastAssignedUnit, lodBiasLocation, lastAssignedLodBias`. Built once per link in
`CacheResourceLocations` (`Managers.cpp:10264-10345`), which also does the array-element
subscripting fix (`SubscriptUniformNameForElement`, `Managers.cpp:10245-10256` — reflection names an
array after its first element at every location it spans, so `goku[7]` needed its own
`glGetUniformLocation`).
### 3.4 `SyncToBackend` — what it reads from the `ProgramObject`
`Managers.cpp:9230-10240`. The complete read set with line numbers (the P4a archive must supply
every one of these, or the call must carry it):
```
9241 GetExternalIndex 9248 GetLinkStatus / GetSpirvStatus
9268 GetShaderStorageBlockBindingOverrides (-> SpvcSession::SetShaderStorageBlockBinding)
9335 GetLinkedShaderStages 9336 GetGeneratedSpirv 9347 PointSizeDemoted
9357 GetLinkedShaderSnapshot 9366 GetSpirvValidationEnabled
9375 GetTransformFeedbackVaryings 10017 GetTransformFeedbackVaryingCount
10067 GetTransformFeedbackBufferMode 10217 GetUBOSize
10234 GetLinkVersion 10235 GetImageUnitVersion
10290 GetActiveUniformBlocksCount 10295 GetUniformBlockName
10310 GetMaxUniformLocation 10312 GetUniformName 10314 GetUniformType
8119 GetShaderStorageBlockBindingOverrides (signature)
8346-8351 GetMaxUniformLocation/GetUniformName/GetUniformType/GetUniformTypeFacts
8377 GetUniformSamplerOrImageUnitIndex 8506/8513 GetUniformLocation / UniformLocationsAliasSameUniform
622/627 (RebindImageUniformsToFrontendUnits) GetUniformLocation / GetUniformSamplerOrImageUnitIndex
```
`ARCHITECTURE.md:264` names the **eight extra inputs** the backend program depends on beyond the
artefacts, and the list above matches: draw-FBO snorm/unorm clamp masks, fragColor broadcast count,
storage-block binding signature, atomic-counter set, live image formats, patch parameters (+ Magma's
default-FB height and XFB layout). `create_shader_state` publishes the **artefacts**; the server
specialises at verb time. That is the design; nothing in this tree contradicts it.
### 3.5 Sampler/LOD-bias/UBO/atomic-counter binding at draw time
`BindCurrentProgramWithResources` (`DirectGLES.cpp:3765-4041`), in order:
1. `backendProgram.Use()` (`:3781`; the impl is `Managers.cpp:10347-10390`, with the
`m_rebindAfterRelink` rule — a relink replaces the executable behind an unchanged GL name);
2. **global UBO / default uniform block** (`:3786-3856`): `GetUBOSize()`, `GetUBOContentVersion()`,
`MapUBO()`; preferred path is the shared persistent-mapped **UBO ring**
(`UboRingAvailable/UboRingAllocate/UboRingMappedPtr/UboRingBufferId`, `:3805-3831`) with a
per-program `UboRingAllocation` keyed `{contentVersion, ringGeneration, frameSerial, offset}`
(`Managers.h:2003`, `BufferImpl::UboRingAllocation`), and a `glBufferSubData` fallback
(`:3833-3852`). Bytes are accounted to `PipeStats::ByteClass::StageUboGlobal`
(`MG_Util/Metrics/PipeStats.h:55-57`). **This is `set_global_constants`'
(`MGPGlobalConstants{ShaderCso, Version, Blob}`, `MGPipeTypes.h:507-513`, "covers the DEFAULT
UNIFORM BLOCK only (D6)") counterpart** — the ring is on `ARCHITECTURE.md:315`'s
do-not-touch list, so the P4a shape is: the client emits the block image + version, the server
keeps the ring exactly as-is;
3. **named UBOs** (`:3857-3910`): `blockBackendIndices` from the link cache,
`currentProgram->GetUniformBlockBinding(i)`, `MGB_CTX->GetBufferBindingPoint(Uniform, binding)`,
then the P3a buffer clean-probe (`GetBufferResource`/`IsBufferDrawClean`/`EnsureBufferResource`,
`:3884-3894`) and `BindBufferBaseCached`/`BindBufferRangeCached`. This is
`set_shader_buffers(Uniform)` — **P4b, not P4a** (dirty bits 15-17, `Managers.h:717-723` says so
explicitly);
4. **atomic counters** (`:3912-3921`) via `SyncAtomicCounterBuffers(bindings, esslBindingTop)`;
5. the sampler pass (§1.6).
### 3.6 The SPIR-V → ESSL transpile: what is per-program and what is per-context
* **`SpvcSession` is per STAGE, per transpile, and stack-allocated**:
`MG_Util/ShaderTranspiler/SpvcSession.h:89-105` (move-only, non-copyable), constructed at
`Managers.cpp:9019-9020` inside `TranspileSpirvToEssl`. Nothing persists it. Its mutators that
carry program state: `SetShaderStorageBlockBinding` (`SpvcSession.h:120`, called
`Managers.cpp:9037-9039`), `SetAtomicCounterBlockBindings` (`SpvcSession.h:135`, called
`Managers.cpp:9060-9061`, and **both halves are memo state** — the top is key, the returned
bindings are payload, `:9051-9059`), `SetVertexAttribLocation` (`:109`),
`DropDefaultFragmentOutputColorIndex` (`:154`), `RelaxReadWriteExclusiveStorageBuffers` (`:175`),
`ReflectTransformFeedbackCaptures` (`:189`), `StripTransformFeedbackDecorations` (`:201`),
`SetEntryPoint` (`:202`), `SetSpecializationConstants` (`:210`).
* **The L2 ESSL translation cache is per PROCESS**: `GetEsslTranslationCache()`
(`MG_Util/ShaderTranspiler/TranslationCache.h:698`), consulted at `Managers.cpp:9664-9720`.
Key inputs `EsslTranslationKeyInputs` (`TranslationCache.h:635-689`) mix **driver capability bits**
(`viewportIndexLoweringArmed`, `supportsNoperspectiveInterpolation`,
`supportsExtendedImageFormats`, the four sample-count limits) with **per-program/per-context**
inputs (`xfbCaptureBlockNames`, `glFormatByUniformName`, `storageBlockBindingOverrides`,
the two block-rename maps, the two location-strip flags, `atomicCounterEsslBindingTop`,
`esslVersion`, `enableSpirvValidation`). Payload `EsslTranslationResult` (`:618-632`) carries the
ESSL **plus** `flattenedXfbBlockNames` **plus** `atomicCounterGlBindings` — the header records
why dropping either is a silent-wrong-output bug. `Managers.cpp:8556-8558` states the invariant:
*every* read inside `TranspileSpirvToEssl` must appear in `BuildEsslTranslationKey`.
`MG_Test/ShaderTranspiler/TranslationCacheTest.cpp` is its unit test.
* **`InvalidateCompileEnv()` is a forwarded (F-class) `PipeInputs` accessor**
(`MG_Backend/MGPipe/PipeInputs.h:573`), i.e. already a reverse call rather than a state read.
`ARCHITECTURE.md:258-260`: glslang stays entirely on the **client**, SPIRV-Cross
(`TranspileSpirvToEssl`) entirely on the **server**, file-level cut. No `MOBILEGL_IPC_PROGRAM`
switch, no server-side compile pool.
### 3.7 What a shader CSO on the wire must carry
`MGPipeTypes.h:320-335`:
```cpp
struct MGPProgramDesc { // 192 B
MGPipeHandle Cso;
Uint32 StageMask; // == GetLinkedShaderStages()
Uint32 GlobalUboSize;
Uint32 ReservedNumSamplesOffset;
Uint8 SpirvStatus, NativeFloat64, PointSizeDemoted, EnableSpirvValidation;
MGPBlobRef Spirv[6]; // per stage
MGPBlobRef Reflection; // the whole LinkArtifacts + SpirvArtifacts archive
};
```
`ARCHITECTURE.md:259` enumerates what the archive **must** cover: the four `ResourceReflection`
vectors (each with its `TypeFacts`), `uniformSamplerOrImageUnitIndex`, `uniformBlockBinding`,
`shaderStorageBlockBinding` (by name), `explicitOpaqueUniformBindings`,
`xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture`, `computeLocalSize`, the GS/TCS/TES
facts, `usesReservedNumSamples`, `uniformOffsets`; and `XfbVarying` carries two spellings (GL name
+ block instance/member/element).
### 3.8 `ProgramArtifacts.h` — the archive as it stands
`MG_State/GLState/ProgramState/ProgramArtifacts.h`, 576 lines, extracted at P0.5. Purity is asserted
by `check_include_closure.py`'s `artifacts-header` probe (`:16-19`), and the header notes that it is
glslang-free **by symbol**, not textually (`:10-15`).
* `TypeFacts` (`:31-59`, 20 fields, POD, `sizeof == 44` pinned at `:552`);
* `ResourceReflection` (`:63-86`, 14 fields, `sizeof == 128` on libstdc++);
* `XfbVarying` (`:95-121`, 11 fields, `sizeof == 128`);
* `LinkArtifacts` (`:160-345`, **58 members**, `sizeof == 1056`), including the live
`SharedPtr<glslang::TProgram> program` at `:165` that is **null for every archived instance by
construction** and is the one member `VisitFields` deliberately omits (`:471-474`);
* `SpirvArtifacts` (`:365-402`, 8 fields, `sizeof == 88`): `generatedSpirv`,
`enableSpirvValidation`, `uniformOffsets`, `globalUboScratch`, `reservedNumSamplesOffset`,
`spirvStatus`, `nativeFloat64`, `pointSizeDemoted`.
* **The visitor tables**: `VisitFields(TypeFacts)` `:408-434`, `(ResourceReflection)` `:436-453`,
`(XfbVarying)` `:455-469`, `(LinkArtifacts)` `:471-535` (57 fields), `(SpirvArtifacts)` `:537-547`.
Contract at `:404-407`: `v(const char* name, Field&)`, `Field` const when `Self` is const, so **one
table serves both directions**.
* **Trip wires** `:549-575`: `sizeof` static_asserts, pinned **per STL** — libstdc++ numbers are in
the tree, the libc++/NDK branch at `:565-566` is **inert until the integrator pins it**
(`MGL_ARTIFACT_SIZES_LIBCXX_PINNED`). `MG_Test/Program/ProgramArtifactsTest.cpp` records every
`sizeof` as a ctest property on every platform.
> **The serializer does not exist.** Grep finds no writer/reader over these tables — only the tables
> and the size asserts. Every `VisitFields` comment says "(and its serializer when one exists)".
> **P4a's contract package owns writing it**, and the NDK `sizeof` pin at `:565-566` is a
> gate the integrator must close in the same phase, or an Android build silently has no trip wire.
### 3.9 Program-pipeline composites (`ProgramPipelineScenario`)
`GLContext::GetProgramForDraw()` (`MG_State/GLState/Core.h:190`, `Core.cpp:609`) already flattens a
pipeline into one hidden composite `ProgramObject` **entirely in the frontend**, which is why
`ARCHITECTURE.md:212` says the tracker pushes **one** handle out of the composite band and the
server never learns it is a composite. Two signatures decide the work
(`MG_State/GLState/ProgramState/ProgramPipelineObject.h`):
* `ComputeDrawProgramSignature` (`:97-107`) — `{lifetimeId, GetLinkVersion()}` per graphics stage;
the header (`:75-85`) records that keying on `GetBackendStateVersion()` instead made the SSO
conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) **on every draw**;
* `ComputeUniformMirrorVersions` (`:125-141`) — the per-stage refresh gate, mixing
`backendStateVersion<<32 | uboContentVersion` and `blockBindingVersion<<32 | uniformWriteSetVersion`.
`DirectGLES.cpp:3157-3163` records the accident this removed: `glUniform1i` on an image uniform used
to bump the composite's cache key and therefore produce a whole new twin.
`ProgramPipelineScenario.cpp:1-28` lists the sixteen conformance cases (compute_shader.*,
shader_image_load_store.advanced-sso-*, shader_storage_buffer_object.basic-syntaxSSO /
basic-noBindingLayout) that depend on the flattening. `MG_Test/Program/ProgramPipelineCompositeTest.cpp`
is the unit-level pin.
### 3.10 The "CompositeResolver"
**It does not exist in the tree.** `grep -rn CompositeResolver MobileGL/` returns nothing; the only
two mentions are `docs/Disaggregated/ARCHITECTURE.md:562` (the P2+ file list for `MG_Impl/Pipe/`,
which today holds `CsoCache.h, PipeFill.cpp, PipeFill.h, ResourceTracker.h, SetHashSuppressor.h,
SlotAllocator.{h,cpp}, Tracker.h, VertexInputEmit.h`) and `ROADMAP.md:20` (P4a's own row).
From `ARCHITECTURE.md:212` and `MGPipeHandles.h:84-92`, what it composes is **program + program**:
the pipeline's per-stage `ProgramObject`s → one `ShaderCso` handle out of the reserved top-1/16 band,
with the pipeline cache's eviction releasing the slot, bumping `gen` and emitting
`delete_shader_state`. It is **not** a framebuffer/sampler composer — those are separate
(`set_framebuffer_state`, `bind_sampler_states`). Its side benefit, stated at `:212`: the blocking
`JoinLinkAndSpirv()` leaves the server's draw path. Model it on `MG_Impl/Pipe/CsoCache.h`
(`kMGPipeCsoCacheCapacity = 64` at `:53`, `Acquire` at `:74`, `Counters` at `:57-69`, LRU eviction,
and the `kMGPipeBehaviourNoCsoContentAddressing` negative control at `:30-37`).
---
## 4. Emulations that touch programs — which stay frontend-side
`ARCHITECTURE.md:216` is the rule: *驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在
driver 里 lowering*, and the per-emulation table is `:221-243`.
| emulation | code | verdict |
|---|---|---|
| **fragColor broadcast** | `PrgramImpl::BroadcastLegacyFragColor` (`Utils.h:260`, `Utils.cpp:506`), called `Managers.cpp:9854`; the count is `g_fragColorBroadcastCount` (`Managers.h:2129`) memoised from the draw FBO's `GetDrawBuffers()` at `DirectGLES.cpp:3078-3101`, snapshotted onto the twin at `Managers.cpp:9261` | **SERVER.** It is a post-emission ESSL rewrite (`ARCHITECTURE.md:315` do-not-touch: "SPIRV-Cross 会话与 post-emission ESSL 重写"), keyed on framebuffer state the server already holds after `set_framebuffer_state`. |
| **noperspective** | `ShaderCompiler::EmulateNoPerspectiveForEssl`, `Managers.cpp:8789-8792`, armed on `!g_GLESCapabilities.SupportsNoperspectiveInterpolation` (`:8788`); the arming bit is L2 key material (`TranslationCache.h:661`) | **SERVER.** Driver-capability-armed SPIR-V pass; the client cannot know the arming. |
| **fp64 demotion** | `DemoteFloat64Pass`, decided **per program at link** and recorded as `SpirvArtifacts::nativeFloat64` (`ProgramArtifacts.h:378-390`); read by `BackendObject.h:496`, `Managers.cpp:3522`/`:3924`, DirectVulkan `VertexInputStateFactory.cpp:253`/`:494`, and by the frontend uniform path `GL_Program.cpp:1319`/`:1643`/`:1784` | **CLIENT (already).** The demotion happens before the archive exists; the flag rides in `MGPProgramDesc::NativeFloat64` (`MGPipeTypes.h:329`) so the server knows the layout. The **vertex-side** fp64 narrowing is server-side (`ARCHITECTURE.md:233`), and ID-15's M-3 ruled the P3a `SyncGpuWrites` gap on that stream a deviation — **P4a inherits that open item**. `DoublePrecisionScenario` is the gate. |
| **XFB scatter** | `XfbImpl` in `DirectGLES.cpp:895-1230`: `XfbCaptureTarget` `:895`, `scattered/scatterProgram/scatterCapacityVertices` `:922-924`, `g_scatterBufferId` `:930`, `ReadbackCapturedRanges` `:987`, `BindScatterCaptureBuffer` `:1032`, `ScatterCapturedRecords` `:1068-1140`, armed at `StartPendingTransformFeedback` `:1210` off `program->NeedsScatteredTransformFeedbackCapture()` (`ProgramObject.h:1227` → `xfbNeedsScatteredCapture`) | **CLIENT** — `ARCHITECTURE.md:239` and §8.5. The read-modify-write moves to the client. Note `Managers.h:576-580`: `ReadbackCapturedRanges` and the scatter path are two of the backend-initiated shadow writebacks that bump the buffer-mutation epoch **without** an op — P3a's epoch contract already names them, and a P4a move must not orphan those bumps. |
| **texture LOD bias** | `EmulateTextureLodBias` (`Utils.h:536`, called `Managers.cpp:9855`) + the per-draw `glUniform1f` at `DirectGLES.cpp:3982-4000` | **SERVER** (post-emission rewrite), but the *value* is sampler state and must arrive with the sampler CSO / sampler view. `MGPTextureParams::LodBias` exists (`MGPipeTypes.h:317`) but `SamplerParameters::lodBias` (`MGPipeValueTypes.h:470`) is the one the pass reads. |
| **image format bake / widen / split / array remap** | §2.5, plus `BakeImageFormatQualifiers` (`Utils.h:313`), `RemapImageArrayElementUnits` (`:365`), `SplitReadWriteImageUniforms` (`:519`), `RemoveLayoutBinding` (`:314`) | **SERVER.** `ARCHITECTURE.md:235`: image-bindable 存储加宽/拆分 → server. |
| **pass-through TCS synthesis** | `AttachPassthroughTessControlStage` (`Managers.h:2041-2044`), `BuildPassthroughTessControlEssl` (`Utils.h:408`) | **SERVER**, keyed on `set_patch_state` (already P2). |
Everything in this table except fp64-demotion and XFB scatter stays on the server, which is why the
shader CSO's payload is SPIR-V + archive and **not** ESSL.
---
## 5. Tests
### 5.1 Integration scenarios that already exercise this surface
All rows are in `MG_IntegrationTest/CMakeLists.txt:56-135`.
*Samplers / border colour*: `IntegerBorderColorScenario` (`:127`) — the `borderColorForm` scenario;
its header (`Scenarios/IntegerBorderColorScenario.cpp:9-33`) documents the exact Espryt failure
(1132396544 = the bits of 255.0f read through an integer sampler) and the Vulkan
`VK_EXT_custom_border_color` requirement. **ID-21 records a pre-existing ASan stack-buffer-overflow
in this scenario** — a test bug, and P4a will be running it a lot.
Also `SampledSetStalenessScenario` (`:65`), `TextureViewScenario` (`:114`),
`ClearTexImageUndefinedLevelZeroScenario` (`:128`).
*Images*: `ImageLoadStoreSsoScenario` (`:88`), `ImageTargetKindScenario` (`:89`),
`ImageFormatQualifierScenario` (`:90`), `NonCoreImageFormatScenario` (`:91`),
`ImageSizeAfterRespecScenario` (`:92`), `FormatlessImageBakeScenario` (`:100`),
`UnboundImageDescriptorScenario` (`:126`), `AtomicCounterScenario` (`:119`).
*Programs*: `ProgramPipelineScenario` (`:87`), `PostLinkAttachScenario` (`:99`),
`RelinkStageSetScenario` (`:124`), `AsyncCompileScenario` (`:61`, with two pinned-config lanes at
`:679-753`), `PipelineFailureScenario` (`:68`), `SpirvShaderBinaryScenario` (`:123`),
`UniformInitializerScenario` (`:82`), `Glsl420DeclarationScenario` (`:94`),
`IoBlockNameCollisionScenario` (`:95`), `UnlocatedIoBlockScenario` (`:96`, plus a location-strip
pinned-on lane at `:647-670`), `PointSizeDemotionScenario` (`:109`, pinned lanes `:814-834`),
`SsboDeclarationFormScenario` (`:93`), `SsboArrayLengthScenario` (`:80`),
`SsboArrayDynamicIndexScenario` (`:121`), `DoublePrecisionScenario` (`:81`),
`ViewportArrayScenario` (`:79` + negative control `:768`), the five XFB scenarios (`:104-108`),
`TessellationDrawModeScenario`/`GeometryDrawModeScenario` (`:97-98`).
*The P2/P3a machinery scenarios P4a extends*: `HandleRecycleScenario` (`:133`, six pinned arms at
`:979-1024`), `CsoContentAddressingScenario` (`:134`, four lanes `:1086-1113`),
`ResourceSubsystemControlScenario` (`:135`, two lanes `:1160-1169`),
`PipeVerifyArmingScenario` (`:131`), `PoisonOmissionScenario` (`:132`).
`HandleRecycleScenario` today covers exactly two kinds — buffer
(`ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents`, header `:14`) and VAO
(`AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInput`, `:568`). **P4a's G8
analogue is: add a texture case, a sampler case and a program case, each with `.Handles`/`.Legacy`/
`.AbaControl` arms**, and record the red-before evidence on the contract tree exactly as P3a did.
### 5.2 Unit tests
`MG_Test/Pipe/`: `PipeCatalogueTest.cpp` (payload sizes — `sizeof(SamplerParameters)==100` at
`:131`, trivially-copyable at `:138`), `CsoCacheTest.cpp`, `TrackerTest.cpp`, `SlotAllocatorTest.cpp`,
`PipeInputsTest.cpp`, `RenderStateSpansTest.cpp`, and P3a's two emitter tests
`ResourceEmitTest.cpp` / `VertexInputEmitTest.cpp` (the shape P4a's `SamplerEmitTest` /
`ProgramEmitTest` / `ImageEmitTest` should copy; note ID-14 — `ResourceEmitTest`'s `ApplierGuard`
is the fixture-scoping pattern, and `MG_Test/Buffer/BufferTest.cpp` needed the same treatment).
`MG_Test/ShaderTranspiler/`: `SpirvPassTest.cpp`, `TranslationCacheTest.cpp`,
`WidenImageFormatsTest.cpp`, `FlattenAtomicCounterBlockTest.cpp`, `FlattenXfbInterfaceBlocksTest.cpp`,
`UniquifyIoBlockNamesTest.cpp`, `StripIoBlockLocationsTest.cpp`, `LowerViewportIndexTest.cpp`,
`DemoteFloat64Test.cpp`, `DemotePointSizeTest.cpp`, `LegalizeResourceArrayIndexTest.cpp`,
`ClampMultisampleFetchTest.cpp`, `GlslangCaptureTest.cpp`, `DeriveNumSubgroupsTest.cpp`,
`EmulateSubgroupsTest.cpp`, `FixIterationRP*Test.cpp`, `FlattenFloat64StorageBlockTest.cpp`.
`EmulateNoPerspectiveForEssl` is tested from `MG_Test/Program/ProgramUtilTest.cpp:1344`, `:1385`,
`:1439`, `:1465` (not from the ShaderTranspiler directory).
`MG_Test/Program/`: `ProgramArtifactsTest.cpp` (**the archive's size-and-field pin — P4a's serializer
test belongs beside it**), `ProgramPipelineCompositeTest.cpp`, `ProgramInterfaceTest.cpp`,
`ProgramTest.cpp`, `ProgramUtilTest.cpp`, `AsyncLinkTest.cpp`, `AsyncSpirvPhaseTest.cpp`
(reads `GetLinkVersion()` at `:695`/`:701`), `AsyncCompileTest.cpp`, `AsyncTeardownTest.cpp`,
`OptimisticStatusTest.cpp`, `ParallelShaderCompileTest.cpp`, `ShaderCompileAdoptionTest.cpp`,
`TessellationLinkTest.cpp`, `XfbBlockVaryingTest.cpp`, `XfbFrontendOrderInvarianceTest.cpp`.
### 5.3 `PipeInputs` accessors for programs
`MG_Backend/MGPipe/PipeInputs.h`. O-class storage: `m_programForDispatch`, `m_programForDraw`,
`m_transformFeedbackProgram` (`:683-685`), listed in `MGP_INPUT_STORAGE_LIST` at `:108-109`, `:122`,
with accessors at `:534-548`. F-class (forwarded, sticky, no poison check — the declared exception
at `:558-568`): `GetProgramObject(Uint index)` `:570`, `ValidateProgramName(Uint index)` `:574`,
plus `GetTextureObject` `:571` and `InvalidateCompileEnv` `:573`.
`Coverage.def:99-101` maps `GetProgramForDispatch→SetDispatchProgram`,
`GetProgramForDraw→SetDrawProgram`, `GetProgramObject→CreateShaderState`, and `:127`/`:146-149`
records `GetProgramObject`/`ValidateProgramName` as client-resolved name lookups.
`gPipeInputs` is **leak-at-exit storage** (`PipeInputs.h:688-700`, ID-18/ID-21): its O-class
`SharedPtr`s reach `~BufferObject` → the slot allocator at exit. P4a adds **more** frontend
`SharedPtr` members if it is careless — sampler/program handles must be `MGPipeHandle`, not
`SharedPtr`, or the exit-order closure has to be re-litigated.
---
## 6. Traps and open items for the P4a brief
1. **`SamplerParameters` has 3 bytes of trailing padding and no field table.** §1.1. Contract commit
must add `MGP_FIELDS_SamplerParameters` + the `MGP_VERIFY_PAYLOAD_LIST` row, and the sampler CSO's
content hash must be field-wise. A memcmp-based hash mints a fresh CSO per call.
2. **`MGPTextureParams` cannot express what `SyncBuiltinSamplerToBackend` pushes.** §1.8 — no filter,
wrap, compare mode or border colour. Decide in the contract: widen the payload (it is 32 B with
an `MGP_ASSERT_POD`) or give each texture an implicit sampler CSO.
3. **The archive has no serializer, and the libc++/NDK `sizeof` pin is inert.**
`ProgramArtifacts.h:565-566`. Both are contract-package work; the NDK pin must land in the same
phase or the Android build has no trip wire on a 58-member struct.
4. **`g_boundSamplersCache` is a raw twin pointer per unit and is load-bearing for three memos.**
§1.4-1.6. It becomes a per-unit handle array in the applier; the `memcmp` replay proofs
(`DirectGLES.cpp:3597-3604`, `:3942-3950`) have to be re-derived against that.
5. **The sampler twin is created lazily, only from the program pass** (`DirectGLES.cpp:4014-4020`).
`create_sampler_state` inverts the timing of `glGenSamplers`.
6. **Three of the program twin's nine staleness keys are not program state.** §3.2/§3.4. The
`st_variant` specialisation is the design (`ARCHITECTURE.md:264`); a brief that treats
`create_shader_state` as self-contained will produce a per-draw rebuild.
7. **`g_rawDepthFetchSamplerState` constructs a frontend `SamplerObject` inside `MG_Backend`**
(`DirectGLES.cpp:209`). `ARCHITECTURE.md:322` names its removal as P4a work. The purity gate G13
(`grep -rc pGLContext MG_Backend` empty) does not catch it — it is a type use, not `pGLContext`.
8. **The image sweep's gate must stay keyed on the frontend generation** (`DirectGLES.cpp:2195-2201`)
— a server-side epoch is bumped too late by construction.
9. **`SlotTables.h` still mints handles from inside `MG_Backend`** for the six P2 kinds
(`:70-77` records the debt; `GetOrCreate(StatePtr)` at `:209-252` calls
`MGPipeSlots().Acquire`). P3a discharged it for `Buffer` only. P4a discharges it for
`Texture`, `Framebuffer`, `SamplerCso`, `ShaderCso` — four of the remaining five — and should say
whether `Renderbuffer` follows in the same phase (its `resource_*` conversion is in the
`ROADMAP.md:20` cell) or is left for later.
10. **`ForEachLive` needs `Entry::stateRef`, and a handle-keyed entry has none** (`SlotTables.h:266-269`,
`:443-463`). The one direct-iteration site is `ScopedDetachedTextureFramebufferAttachments`, which
walks **texture** twins (`SlotTables.h:61-64`) — i.e. exactly a kind P4a converts. That iteration
has to be re-expressed before the texture table goes handle-keyed, or it silently walks nothing.
11. **The photon canary.** §2.4 — pin `photon-v1.3b` (llvmpipe retrace, **not** Adreno) as a named
P4a case; it is the only thing that has ever caught an image-binding-semantics regression.
12. **`ProgramPipelineObject`'s composite band is allocated but unused** (`MGPipeHandles.h:84-92`).
The composite resolver mints into it, and the pipeline cache's eviction is what frees the slot,
bumps `gen` and emits `delete_shader_state` (`ARCHITECTURE.md:212`).
13. **Inherited from P3a**: ID-15 M-3 (the fp64 stream's dropped `SyncGpuWrites`) is still a ruled
deviation; ID-21's `IntegerBorderColorScenario` ASan stack-buffer-overflow is still open and sits
squarely in P4a's sampler test set; ID-19's per-draw vertex-input emission cost (+699/+422
ns/draw) is on the optimisation list and P4a's per-unit sampler/image emission is the same shape
of risk on VAO/texture-heavy frames (rd12 was +30%/+27%).
14. **Subsystem bits.** `MGPipe.h:72-95` has bits 0-8 used and
`kMGPipeSubsystemsMigratedAtP3a = 0x1ff`. P4a needs its own bits (framebuffer / texture+sampler /
program are three natural ones) and a `kMGPipeSubsystemsMigratedAtP4a`, plus the arm-resolution
treatment at `Managers.cpp:2352-2400` (`ClassifyPipeSubsystemArm`, `StopOnArmlessPipeSubsystem`)
— including the **dependency refusal** pattern (bit 8 requires bit 7, `Managers.cpp:2395-2400`):
a program/sampler bit will require the texture bit for the same reason.
15. **Dirty bits.** `Tracker.h:70-83` already computes `NewShader`, `NewShaderBindings`,
`NewGlobalConstants`, `NewFramebuffer`, `NewSamplerViews`, `NewSamplers`, `NewShaderImages`
(values at `:246-262` and `:305-312`) — P4a only has to **emit** them and extend
`kMGPipeDirtyEmittedAtP3a` (`:104-107`) into a `…AtP4a`. `MGPipeSubsystemForDirty` (`:128`ff)
is where each new bit's gate goes.
16. **Registration.** `MGPipeSetResourceOps` is installed from `BufferImpl::RegisterBufferBackendOps`
(`Managers.cpp:2529-2551`, called `BackendObject_DirectGLES.cpp:849` and
`DirectGLES.cpp:10568`) — ID-8's accepted deviation. P4a's new op tables should get their own
registration entry point rather than piling onto the buffer one.