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

816 lines
59 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 — Espryt's framebuffer / renderbuffer / texture surface that P4a converts
Tree: `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`, branch `feat/disaggregated`
@ `37da3c3a` (P3a docs commit). Every line number below was opened in this tree. Paths are repo-relative
from `MobileGL/` unless stated.
Files that carry essentially all of it:
| file | lines | what P4a touches |
|---|---|---|
| `MG_Backend/DirectGLES/DirectGLES.cpp` | 11142 | `SyncCurrentFBO`, `BindCurrentFBO`, `ForceBindCurrentFBO`, `SyncAndBindFramebufferObject`, the two texture sync lists + the unit-bindings epoch, image units, readbacks, blits, CopyImage, GenerateMipmap, the scoped state guards |
| `MG_Backend/DirectGLES/Managers.cpp` | 10698 | `BackendFramebufferObject`, `BackendTextureObject`, `BackendRenderbufferObject`, `ScratchFBOImpl`, `PixelStoreImpl`, the unpack ring staging, the death-notice consumer |
| `MG_Backend/DirectGLES/Managers.h` | 2260 | the twin classes' members and the `TwinRegistry` alias / `StateBackendObjectRegistry` two-arm template |
| `MG_Backend/DirectGLES/SlotTables.h` | 577 | `BackendSlotTable` — the `{slot,gen}` table, both `GetOrCreate` overloads, `ReleaseByHandle`, the holder list |
| `MG_Backend/DirectGLES/Utils.cpp/.h` | 2405 / 543 | format caveat/widening predicates, `ReadbackImpl` client-store walks |
| `MG_Pipe/MGPipeTypes.h`, `MG_Pipe/PipeCalls.def` | — | the payloads P4a must fill (§7) |
---
## 0. The contract P4a is filling in (already written, all stubs today)
`MG_Pipe/PipeCalls.def` rows that P4a owns (none are new; only their emitters/appliers are):
- `:113` `X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone)`
- `:118` `X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail)`
- `:119` `X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail)`
- `:120` `X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail)`
- `:133` `X(SetTextureParams, MGPTextureParams, kCtxObject, kNone)`
- `:140` `X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone)`
- `:141` `X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot)`
- `:139` `X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone)`
- `:142/:143` `X(Blit, MGPBlit, kCtxVerb, kNone)`, `X(Clear, MGPClear, kCtxVerb, kNone)`
- `:144` `X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot)`
- CSO create/delete: `:105-108` `CreateSamplerState` / `DeleteSamplerState` / `CreateSamplerView` / `DeleteSamplerView`.
Payload shapes (`MG_Pipe/MGPipeTypes.h`), with the fields that decide P4a's work:
- `MGPResourceDesc` `:150-175`, 88 B. **One create/respecify shape for buffers, every texture target and
renderbuffers** — `Target`, `StorageKind` (== `TextureStorageType`), `BindMask`, `InternalFormat`,
`Width/Height/Depth`, `ArrayLayers/Levels/Samples`, `FixedSampleLocations`, `Immutable`,
`HasDefinedContent`, **`ImageBindableHint` (`:166`, "client-side everImageBound; pre-emptive
allocation")**, `ViewOf` (`:173`, texture-view storage owner), `BufferForTexBuffer` + `BufOffset/BufSize`
(`:174-175`) for a buffer texture. So texture creation reuses P3a's `resource_create/respecify`.
- `MGPSurface` `:342-352`, 24 B: `Res`, `InternalFormat` **inline** (so the cross-object masks fall out with
no lookup), `Kind` (Texture|Renderbuffer|None), `Layered`, `Level`, `Layer`, `UploadTarget`.
- `MGPFramebufferState` `:354-371`, 304 B: `Fbo` (`kMGPipeDefaultFramebuffer` for the default FBO),
`Color[8]`, `Depth`, `Stencil`, **`ReadSurface` — "the RESOLVED read surface, not an index. This is what
structurally closes the read-buffer-shared-FBO defect class" (`:359-361`)**, `DrawBuffers[8]` as
`Int8` attachment indices with **-1 = NONE**, `Width/Height/Layers/Samples`,
`FixedSampleLocations/IsDefault/Complete`, and `ContentHash` (`:369`) doing two jobs: the server's
render-pass memo key and the **client-side emission suppressor**.
*Note for the implementer:* `Color[8]` and `DrawBuffers[8]` are **8**, while the frontend today carries
`FramebufferObject::MAX_DRAW_BUFFERS` draw buffers and `Color0..Color31` attachment points
(`Managers.h:1579-1581` computes `MAX_COLOR_ATTACHMENT_SLOTS` as `Color31-Color0+1` = 32). The
attachment-relocation table (§1.4) is 32 wide. This is a real width mismatch P4a has to rule on.
- `MGPSamplerView` `:288-303`, 36 B: "ONLY the view restrictions. Everything a `glTexParameter` writes lives
on `set_texture_params` instead, **because a texture that is only an FBO attachment, only an image binding
or only a `glCopyImageSubData` endpoint has no sampler view to hang it on**" (`:288-291`). Carries
`InternalFormat` (aliasing format for `glTextureView`), `Target`, `MinLevel/NumLevels/MinLayer/NumLayers`,
`Samples`, `FixedSampleLocations`.
- `MGPTextureParams` `:306-318`, 32 B: `Res`, `BaseLevel/MaxLevel`, `Swizzle[4]`, `DepthStencilMode`,
**`ForceResync` — "Mirrors `m_forceTextureParamsResync`" (`:312-313`)**, `MinLod/MaxLod/LodBias`.
- `MGPBoundView` `:428-434` / `MGPSamplerViews` `:436-440`; `MGPImageView` `:443-452` /
`MGPShaderImages` `:454-458`; `MGPSamplerStates` `:442-446`.
- `MGPCopyRegion` `:708-716` (64 B), `MGPBlit` `:718-727` (56 B), `MGPClear` `:729-739` (48 B),
`MGPMipPlan` `:741-745` (16 B), `MGPReadbackInfo` `:748-756` (64 B, `Res` null for `read_pixels`:
"the bound read surface answers").
`MGPTextureParams` has **no border colour and no filter/wrap/compare/anisotropy**; those live in
`MGPSamplerDesc` (`:283-287`, a blob of `SamplerParameters`) — but Espryt today pushes filters/wrap/LOD/
anisotropy **onto the texture object** through `SyncBuiltinSamplerToBackend` (§2.4). That mapping
(built-in sampler → sampler CSO, or → an extra params field) is P4a's second contract ruling.
---
## 1. How Espryt resolves the current draw/read framebuffer today
### 1.1 The FBO twin and its table
`FramebufferImpl::BackendFramebufferObject` — `Managers.h:1537-1610`.
Members (all of them are the server-side state P4a must relocate):
| member | line | meaning |
|---|---|---|
| `m_backendFBOId` | `Managers.h:1561` | driver FBO name, `glGenFramebuffers` in the ctor (`Managers.cpp:7015`) |
| `m_contextGeneration` | `:1562` | ES context generation the id belongs to; dtor only deletes when it matches (`Managers.cpp:7035`) |
| `m_frontendDrawBuffers[MAX_DRAW_BUFFERS]` | `:1570-1571` | the app's draw-buffer array verbatim (holes / reversals allowed) |
| `m_backendDrawBuffers[MAX_DRAW_BUFFERS]` | `:1577` | the ES-legal compacted array actually handed to `glDrawBuffers` |
| `m_backendColorSlots[32]` | `:1593` | **the permutation**: where frontend `COLOR_ATTACHMENTn` physically lives as a backend `COLOR_ATTACHMENTm` |
| `m_frontendReadBuffer` / `m_backendReadBuffer` | `:1600-1601` | read-buffer memo |
| `m_syncedFrontendAttachmentVersions` | `:1604` | per-attachment-point memo, keyed on FRONTEND attachment versions |
| `m_syncedBackendIdGeneration` | `:1609` | `g_attachmentBackendIdGeneration` at the last attachment walk |
Table: `extern TwinRegistry<FramebufferObject, BackendFramebufferObject, MGPipeKind::Framebuffer>
g_backendFramebufferObjects;` — declared `Managers.h:1612-1613`, defined `Managers.cpp:7754-7755`.
`TwinRegistry` is the alias at `Managers.h:547-553` that swallows the kind in the pull build;
`StateBackendObjectRegistry` (`Managers.h:303-540`) dispatches to `m_slotTable`
(`BackendSlotTable`, `Managers.h:538`) when `EsprytSlotTablesEnabled()` (`SlotTables.h:132-135`),
otherwise to the legacy `UnorderedMap<StateObject*, Entry>` (`Managers.h:319`).
The other two tables of this family:
- `TextureImpl::g_backendTextureObjects` — `Managers.h:1521-1522`, defined `Managers.cpp:7001`.
- `RenderbufferImpl::g_backendRenderbufferObjects` — `Managers.h:2257-2258`, defined `Managers.cpp:10695-10696`.
- (`SamplerImpl::g_backendSamplerObjects` — `Managers.h:2230-2231`, defined `Managers.cpp:10583`.)
All four still **mint** their handle inside `MG_Backend` off a frontend `GetLifetimeId()`
(`SlotTables.h:231-232`) — that is the debt `SlotTables.h:70-77` records against itself and that
P4a discharges for these kinds, exactly as P3a's seventh table (`BufferImpl::BackendBufferResourceTable`,
`Managers.h:690-692`) already does with `GetOrCreate(MGPipeHandle)` (`SlotTables.h:278-326`) and
`ReleaseByHandle` (`SlotTables.h:350-361`).
### 1.2 The frontend binding slot read
`GetFramebufferBindingSlotChecked(FramebufferTarget)` — `DirectGLES.cpp:173-193`. On the handle arm it is a
plain `MGB_CTX->GetFramebufferBindingSlot(target)` (`:176`); on the legacy arm it goes through the raw
pointer cache `g_fbSlotCache` / `g_fbSlotCacheContext` (`:156-157`, `:179-188`), which the comment at
`:159-172` explains was a **poison/verify bypass** and is therefore kept only for the pull control.
Five call sites: `:1944`, `:2242`, `:3080`, `:3210`, `:3300`.
### 1.3 `SyncCurrentFBO` — `DirectGLES.cpp:2229-2315`
Sequence, verbatim:
1. `:2233-2235` — three `CollectGarbageIfNeeded()` ticks (framebuffer, texture, renderbuffer). These are
no-ops on the handle arm (`Managers.h:477-491`).
2. `:2237` — walks `{Draw, Read}` in that order; `lastUpdatedFBO` carries the draw object forward.
3. **The four-part memo**, `:2254-2266`:
`slotVersion` (`slot.GetVersion()`), `objectVersion` (`currentFBO->GetObjectVersion()`), the raw
`currentPtr` (identity only), and `g_attachmentBackendIdGeneration`. All four stamped together by
`StampSyncedFBO` (`:2221-2227`) into `g_fboSyncedSlotVersions` / `g_fboSyncedObjectVersions` /
`g_fboSyncedObjects` / `g_fboSyncedBackendIdGenerations` (declared `Managers.h:1690-1710`,
defined `Managers.cpp:7756-7762`). The comment at `:2245-2253` records the bug that fixed:
leaving the slot version to `ForceBindCurrentFBO` alone made the early-out never fire and a
Minecraft-shaped frame re-ran the whole attachment walk on all 5495 draws.
4. `:2268-2271` — no FBO bound at all → `MGLOG_E_ONCE`, continue.
5. **Default-framebuffer branch** `:2273-2284`: nothing to sync *except* resetting
`g_alphaWidenedDrawBufferMask` / `g_integerColorDrawBufferMask` to 0 for the DRAW target, then stamp.
6. **Same-FBO-as-draw skip** `:2286-2302`: when the READ target names the object already synced as DRAW,
the attachment/draw-buffer work is skipped but `SyncReadBufferToBackend` is still called
(`:2295-2298`), through a `Find` on the twin table. This is the `read-buffer shared FBO` fix
(`draw_buffers_1`); `MGPFramebufferState::ReadSurface` is the structural replacement.
7. `:2304-2310` — resolve-or-create the twin, materialise it if empty, `SyncToBackend(currentFBO, target)`.
### 1.4 `BackendFramebufferObject::SyncToBackend` — `Managers.cpp:7521-7737`
- `:7533` `Bind(asTarget)` first.
- Draw buffers `:7537-7585`: memcmp against `m_frontendDrawBuffers`; **applied only when
`asTarget == Draw`** (`:7551`) — the comment `:7544-7550` records the Minecraft 26.x OIT bug where a
READ-only sync would land `glDrawBuffers` on the wrong framebuffer and falsely stamp the memo.
`FramebufferAttachmentType::None` → `GL_NONE` (`:7558-7560`) — **that is the "draw-buffer None"
handling**; default-framebuffer FRONT/BACK tokens pass through unremapped (`:7564-7569`);
everything else is forced to `GL_COLOR_ATTACHMENT0 + i` (`:7571`), and `nEffectiveBuffers` is the
highest non-None index + 1 (`:7573`).
- `RecomputeBackendColorSlots` — `Managers.cpp:7432-7519`. Three passes: draw-buffer slot `s` pins
attachment `a` to backend point `s` (`:7453-7474`); survivors keep identity (`:7479-7486`); displaced
attachments are parked on the lowest free point (`:7492-7504`). Returns "moved", and each moved
attachment has its version memo forced to `~0` (`:7515-7516`). When anything moved the read-buffer memo
is dropped (`:7580-7582`). `GetBackendAttachmentType` (`:7739-7752`) is the single reader.
- Draw-target-only mask recompute `:7587-7619`: `snormClampOutputMask`, `unormClampOutputMask`,
`g_alphaWidenedDrawBufferMask`, `g_integerColorDrawBufferMask` (globals declared `Managers.h:1650`,
`:1659`, defined `Managers.cpp:7308-7309`; the shader-side masks go into
`PrgramImpl::g_snormFallbackClampOutputMask` / `g_unormFallbackClampOutputMask`).
- Read buffer `:7623-7625` → `SyncReadBufferToBackend` (`Managers.cpp:7410-7430`), which binds as READ
first (`:7427`) then `glReadBuffer`.
- **Attachment walk** `:7627-7724`: re-arm every point when `m_syncedBackendIdGeneration !=
g_attachmentBackendIdGeneration` (`:7632-7636`); per point compare
`m_syncedFrontendAttachmentVersions[i] != attachmentVersions[i]` (`:7650`); an empty colour point below
`MaxColorAttachments` is explicitly detached with `glFramebufferRenderbuffer(..., 0)` (`:7659-7666`) —
the comment `:7651-7658` says removing that detach breaks the permutation invariant.
- **Re-entrancy** `:7726-7736`: the walk itself can re-mint a texture id, so `SyncToBackend` recurses until
`g_attachmentBackendIdGeneration` is quiescent.
`SyncAttachmentObject` — `Managers.cpp:7142-7216`: the attachment id/layer/level resolution.
- Texture branch `:7145-7194`: find-or-create the texture twin, **`SyncMipmapsToBackend` only**
(`:7161` — see §2.6, the parameter gap), then three shapes:
layered → `glFramebufferTexture(target, att, id, level)` (`:7162-7165`);
3D / 2DArray / 1DArray / CubeMapArray / 2DMSArray → `glFramebufferTextureLayer(..., level, layer)`
(`:7166-7177`); otherwise bind + `glFramebufferTexture2D` with the cube-face target
(`:7178-7194`, the cube-face bind rule at `:7184-7190`).
- Renderbuffer branch `:7195-7214`: find-or-create, `SyncToBackend`, `Bind()`,
`glFramebufferRenderbuffer`.
- An empty attachment returns `true` and issues nothing (`:7215`).
Read-side helpers on the frontend READ binding: `GetReadColorAttachment` (`Managers.cpp:7280-7291`),
`IsFixedPointFallbackReadAttachment` (`:7385-7408`), `IsAlphaWidenedFallbackReadAttachment` (`:7377-7383`),
`IsAlphaWidenedColorAttachment` (`:7293-7306`), `ComputeAlphaWidenedDrawBufferMask` (`:7357-7372`),
`IsIntegerColorAttachment` (`:7344-7355`), `IsSnormFallbackAttachment` / `IsUnormFallbackAttachment`
(`:7246-7276`).
### 1.5 Binding: `BindCurrentFBO` / `ForceBindCurrentFBO` / `SyncAndBindFramebufferObject`
- `BindCurrentFBO(target)` — `DirectGLES.cpp:3206-3260`. **No fast path on the slot version** — the
comment `:3211-3216` records `KHR-GL32.packed_pixels` reading out of the previous subtest's framebuffer.
Handle arm: a plain `Find` (`:3229-3232`); legacy arm: the 64-slot `g_fboTwinLookupMemo`
(`DirectGLES.cpp:140-141`, used `:3237-3243`). Default FBO → `BindFramebufferId(..., 0)` **through the
shadow** (`:3253-3259`) — *"a raw bind here would leave the shadow claiming the previous user FBO,
false-skipping its next re-bind"*. **This is the default-FBO-bind trap from the readback-state-guards
work**; its siblings are `SyncAndBindFramebufferObject`'s early return (`:3267-3280`) and
`NoteFramebufferIdDeleted` (`Managers.cpp:7096-7105`).
- `SyncAndBindFramebufferObject(fbo, target, forceSync)` — `DirectGLES.cpp:3262-3294`; used by the DSA
clears and `BlitNamedFramebuffer`. `forceSync` → `InvalidateSyncedState()` (`Managers.cpp:7119-7140`).
- `ForceBindCurrentFBO(target)` — `DirectGLES.cpp:3296-3308`: sync+bind then stamp all four memo arrays by
hand.
Driver-binding shadow: `g_driverFBOBindings` / `g_driverFBOBindingKnown` (`Managers.cpp:7051-7056`),
`BindFramebufferId` (`:7058-7079`), `CurrentFramebufferBinding` (`:7081-7094`, the one `glGetIntegerv`
cold pin), `NoteFramebufferIdDeleted` (`:7096-7105`), `InvalidateFramebufferBindingCache`
(`:7107-7117` — also clears the three synced-FBO arrays; called from `DirectGLES.cpp:5597`, `:6112`,
`:6211`, `:10133`, `:10577`, `:11110`).
Call sites that drive the pair (`SyncCurrentFBO` then `BindCurrentFBO`): `PrepareForDraw`
(`DirectGLES.cpp:3359` + `:3363`), `Clear` (`:4511` + `:4518`), `BlitFramebuffer` (`:6433`, `:6442-6443`),
`CopyTexImage2D` (`:7176` + `:7187`), `CopyTexSubImage2D` (`:7270` + `:7282`), `ClearBufferfi/fv/iv/uiv`
(`:7866/7912/7925/7941` + `:7869/7915/7931/7944`), the four `ClearNamedFramebuffer*`
(`:7969`, `:7986`, `:8006`, `:8026` via `ForceBindCurrentFBO`), `ReadPixels` (`:9510` + `:9513`),
`GetTexImage` (`:9681`), the multisample-resolve fallback (`:6507-6508`).
### 1.6 The fragColor MRT broadcast
`PrgramImpl` `DirectGLES.cpp:3037-3057`: the memo `g_broadcastMemoFbo` / `g_broadcastMemoSlotVersion` /
`g_broadcastMemoObjectVersion` / `g_broadcastMemoValid` / `g_broadcastMemoCount`, **keyed exactly like
`SyncCurrentFBO`'s trio** and cleared by `InvalidateBroadcastMemo()` (`:3055-3057`, called from
`DestroyEGLContext` `:11113`). Derivation `DirectGLES.cpp:3079-3102`: walk `drawFBO->GetDrawBuffers()`,
`enabledDrawBuffers = i+1` for every non-`None` slot, `g_fragColorBroadcastCount = max(count,1)`.
It is **read from the frontend, deliberately not from the backend FBO sync** (`:3076-3078`), because a
program compiled against a stale count would not be relinked until the following draw. It then enters the
program-staleness condition at `:3154` (`twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount`),
i.e. **the draw-buffer array is a shader-compilation input**. P4a must keep that ordering when the FBO
state becomes a pushed `MGPFramebufferState`.
### 1.7 The other emulations that live in this surface
- **Alpha-widened three-channel attachments** (`Managers.h:1642-1659`): the stored alpha is pinned at 1.0
— clears write 1.0 (`SubstituteWidenedClearAlpha`, `Managers.h:1670-1680`, used
`DirectGLES.cpp:7917-7920`, `:7933-7936`, `:7946-7949`, and for DSA at `:7961-7963` via
`IsWidenedNamedDrawBuffer` `:7894-7907`), draws get their alpha write mask forced off
(`RenderStateImpl::SyncRenderState(forColorClear)`, contract at `DirectGLES.h:209-226`, the applied
mask memo `g_syncedColorMaskAlphaWidenMask` at `DirectGLES.cpp:2346`), and readback overwrites alpha
(`IsAlphaWidenedFallbackReadAttachment` → `forceOpaqueAlpha`, `DirectGLES.cpp:9533-9543`).
`g_integerColorDrawBufferMask` exists because `glClearBufferfv` on an integer buffer is
`INVALID_OPERATION` (`Managers.h:1652-1658`).
- **SNORM / UNORM caveat formats**: `IsSnormFallbackAttachment` / `IsUnormFallbackAttachment`
(`Managers.cpp:7246-7276`) → the two shader clamp masks; readback clamp
`applyFixedPointReadClamp` (`DirectGLES.cpp:9542`, the clamp itself `:9204-9231`).
- **Layered attachment shapes**: `attachmentObject.IsLayered()` (5 reads),
`GetTextureLayer()` (1), `GetTextureLevel()` (6), `GetTextureUploadTarget()` (4) — the three-way branch
at `Managers.cpp:7162-7194`; the layered-blit substitute at `DirectGLES.cpp:6265-6421`.
- **Blit LOD / layered blit**: `BlitFramebuffer` `DirectGLES.cpp:6423-…`; the non-zero destination array
layer is served by `glCopyImageSubData` (`:6288`, `:6354-6413`) because a driver may ignore the layer
(`:6387`). The multisample replicate path builds its own programs (`:5889-5910`, `:6105-6191`) and the
aspect split is at `:6218-6258`. `EmulateTextureLodBias` (`Utils.cpp:1749-1830`, `Utils.h:527-536`)
is the shader-side LOD-bias emulation, invoked `Managers.cpp:9855`, gated by
`ShouldAvoidExplicitLodBiasOnAngleLlvmpipe()` (`Managers.cpp:94-97`).
- **D24S8 dual-mode sampling**: `GL_DEPTH_STENCIL_TEXTURE_MODE` is forwarded per texture
(`Managers.cpp:6956-6977`; cache `m_cacheDepthStencilTextureMode`, `Managers.h:1501`), and the *second*
half — one D24S8 sampled through its own name as `STENCIL_INDEX` and through a `glTextureView` as
`DEPTH_COMPONENT` in one pass (Better Clouds) — is why `GL_ARB_texture_view` is only advertised behind
a host extension **and** `MG_Config::Features.EsprytEnableTextureView`
(`BackendObject_DirectGLES.cpp:1177-1203`; the known break is `SyncTextureViewToBackend` normalising the
view's ES internalformat independently of the storage, `:1189-1196`).
The depth/stencil **readback** emulation is `DepthStencilSamplingReadImpl`
(`DirectGLES.cpp:8117-8578`, entered from `:8617` and `:8721`), with `ScopedDepthBlitState` at `:6985`.
---
## 2. The texture object twin
### 2.1 The class
`TextureImpl::BackendTextureObject` — `Managers.h:1341-1517`. Public surface `:1343-1417`; the members
that are the real server state:
| member | line | note |
|---|---|---|
| `m_backendTextureId` | `1422` | driver name |
| `m_bufferImageSplitViewId` | `1441` | second buffer-texture name for `glBindImageTexture` (the rg32f→r32f split) |
| `m_viewSourceBackendTextureId` | `1447` | for a `glTextureView`: the ES name it was made a view OF |
| `m_contextGeneration` | `1455` | |
| `m_isInitialized`, `m_imageBindableStorageRequired`, `m_backendStorageImmutable` | `1456-1458` | |
| `m_bufferTextureUnsupportedReported` | `1463` | one-shot report latch |
| `m_prevTextureInfo` (`StateTextureBasicInfo`, `Managers.h:1320-1338`) | `1464` | format/w/h/d/levels/bufferExternalIndex/samples/fixedSampleLocations |
| `m_syncedContentVersion` | `1468` | |
| `m_syncedShapeContextId`, `m_syncedShapeGeneration`, `m_syncedShapeParamsVersion` | `1481-1483` | the cheap-gate trio |
| `m_cacheSamplerParameters`, `m_cacheLodRange` | `1484-1485` | |
| `m_cacheBorderColor` / `I` / `UI` / `Form` | `1490-1493` | **all four**, because none alone identifies the driver's border colour |
| `m_cacheSwizzleParams` | `1494-1495` | |
| `m_cacheDepthStencilTextureMode` | `1501` | |
| `m_syncedSamplerVersion`, `m_syncedTextureParamsVersion` | `1502-1503` | |
| `m_forceTextureParamsResync`, `m_forceSamplerResync` | `1507`, `1516` | the two re-mint overrides (`MGPTextureParams::ForceResync` mirrors the first) |
`IsDrawSyncClean(t, contextId, samplingGeneration)` — `Managers.h:1399-1417`: the aggregate gate that is
*exactly* the conjunction of the three per-draw sync calls' own early-outs (documented `:1383-1394`; it
must never be more permissive).
Unit binding shadow: `g_boundTexturesCache[192][TextureTargetCount]` (`Managers.h:1530-1532`, defined
`Managers.cpp:6998-7000`), `g_activeTextureUnit` (`Managers.cpp:6997`), `ActivateTextureUnit`
(`Managers.cpp:6980-6986`), `UnbindTexture` (`:6988-6995`), `Bind(target, unit=TempTextureUnit=0)`
(`Managers.cpp:4729-4743`, `TempTextureUnit` at `Managers.h:1340`).
### 2.2 `SyncTextureObjectToBackend` — `DirectGLES.cpp:1501-1577`
Find-or-create (`:1507-1512`), then a **by-value SharedPtr copy** of the twin
(`:1514-1526`) because the sync can re-enter (a texture view syncs its storage texture first) — on the
slot arm the copy is kept only to hold the twin alive. Then, in order:
`RequireImageBindableStorage` if asked (`:1528-1530`), `SyncTextureParamsToBackend` (`:1531`),
`SyncBuiltinSamplerToBackend` (`:1532`), `SyncMipmapsToBackend` (`:1533`), and — if the storage sync
re-minted the name — **both parameter pushes again** (`:1541-1544`, `NeedsParameterResync()`
`Managers.h:1397`). Handle arm re-resolves once (`:1546-1566`); legacy arm re-resolves and repairs
(`:1568-1576`).
### 2.3 The per-draw work lists and the unit-bindings epoch
`DirectGLES.cpp:1579-1990` is the block P4a rewrites. In order:
- **The identity snapshot.** `UnitBindingsSnapshot` on the push arm holds **lifetime ids, not weak_ptrs**
(`:1594-1622`; `LifetimeIdOf` `:1624-1630`), with the legacy weak_ptr fields kept under
`MOBILEGL_PIPE_LEGACY_MEMOS` (`:1616-1621`) and the arm chosen at **runtime** via
`MGB_UNIT_BINDINGS_HANDLE_ARM` (`:1632-1636`, `#undef` at `:1694`). Pull-build copy of the same two
functions at `:1696-1727`. `CaptureUnitBindings` `:1638-1664`, `UnitBindingsUnchanged` `:1666-1693`.
The design note at `:1579-1593` says explicitly what the snapshot does **not** cover (completeness
flips, high-water mark, context identity, ES context generation, the program keys).
**This is the ~115-line derivation the task names.**
- **`CurrentUnitBindingsEpoch(maxTouchedUnit)`** — `:1746-1773`, over
`g_observedUnitBindings` / `…ContextId` / `…Generation` / `…MaxUnit` / `g_unitBindingsEpoch`
(`:1730-1734`). Gate: `(contextId, bindGeneration, maxTouchedUnit)`; on a miss it re-walks and bumps
the epoch only when the bindings really moved. Stats gate `Gate::EsprytUnitBindingsEpoch` (`:1757`, `:1762`).
- **`UnitTextureSyncEntry`** — `:1801-1805`: `{const SharedPtr<ITextureObject>* slot,
ITextureObject* texture, BackendTextureObject* backend}`. **Entries borrow, never own**
(rationale `:1783-1800`), and `PairingsIntact` (`:1808-1813`) is the structural net under the keys
(a DSA by-name slot swap that never reached the bind generation would otherwise drive texture A's twin
from texture B's state).
- **`g_unitTextureSyncList`** + keys `:1814-1820`
(`Valid`, `ContextId`, `MaxUnit`, `ContextGeneration`, `Epoch`, `SamplingGeneration`).
- **`g_fboTextureSyncList`** + keys `:1825-1830`
(`Fbo`, `SlotVersion`, `ObjectVersion`, `ContextId`, `ContextGeneration`).
- **`DrawTextureSyncKeys`** `:1839-1844` and `CaptureDrawTextureSyncKeys()` `:1846-1858`
(`contextId`, `maxTouchedUnit`, `samplingGeneration`, `unitBindingsEpoch`).
- **`SyncNeccessaryTextures(keys)`** `:1860-1988`.
Unit list hit path `:1879-1901` (per entry: `IsDrawSyncClean` then the three syncs);
rebuild `:1902-1930` (skips `IsUndefinedDefaultTexture`, `:1918`).
**FBO list `:1944-1987` walks the DRAW slot only** (`:1944`), over
`currentFBO->GetAllAttachmentObjects()` with `attachment.IsTexture()` (`:1970-1976`); a null draw FBO
clears the list (`:1984-1987`). Stats gate `Gate::EsprytTextureSyncList` (`:1889`, `:1904`).
No-arg wrapper `:1990` used by every non-draw entry point.
### 2.4 `SyncTextureParamsToBackend` / `SyncBuiltinSamplerToBackend`
- `SyncBuiltinSamplerToBackend` — `Managers.cpp:6671-6780`. Gate `m_syncedSamplerVersion == version &&
!m_forceSamplerResync` (`:6684`). Multisample targets: cache and return without any GL
(`:6706-6709`). Then min/mag filter (`:6732-6744`, via `ResolveBackendMinFilter`), wrap S/T
(`:6749-6750`), wrap R only when `SupportsWrapR` (`:6751-6755`), compare func/mode (`:6756-6757`),
min/max LOD (`:6758-6765`), max anisotropy behind `SupportsTextureFilterAnisotropy` (`:6766-6775`).
**All of it via `glTexParameter*` on the texture object, after `Bind(target)` at `:6711`.**
- `SyncTextureParamsToBackend` — `Managers.cpp:6782-6978`. Gate
`m_syncedTextureParamsVersion == version && !m_forceTextureParamsResync` (`:6794`);
multisample refresh-but-skip at `:6821-6825`; `Bind(target)` `:6827`; base/max level `:6837-6847`;
swizzle with the alpha-widening substitution `:6852-6864` and the image-widening substitution
`:6865-6879`; border colour (all three forms + form, `:~6900-6954`); `GL_DEPTH_STENCIL_TEXTURE_MODE`
`:6956-6977`.
Both `Bind()` on `TempTextureUnit` (unit 0), which is why `BindCurrentTextures` has to re-establish the
real unit bindings afterwards (`DirectGLES.cpp:3687-3693`).
### 2.5 `SyncMipmapsToBackend` — `Managers.cpp:5672-…`
- Texture-view short-circuit `:5683-5686` → `SyncTextureViewToBackend`.
- **Cheap gate** `:5700-5707`: `m_isInitialized && shapeContextId && ctx live && shapeContextId ==
GetTextureContextId() && shapeGeneration == GetSamplingResolutionGeneration() && contentVersion ==
GetContentVersion() && shapeParamsVersion == GetTextureParamsVersion() && StorageType == Mipmap`.
- `IsComplete() || HasAnyDefinedMipmapLevel()` bail `:5734-5738`.
- **Second fast path** `:5751-5778`: content version unchanged + a `StateTextureBasicInfo` probe equal to
`m_prevTextureInfo` → re-stamp the cheap gate and return without the scratch bind.
- `Bind(target)` `:5780`; `currentTextureInfo` built `:5785-5792`.
- `TextureStorageType::Mipmap` `:5794-6503`:
- `needsRegeneration` `:5800`; immutable storage forces `RecreateBackendTexture()` `:5801-5804`.
- `imageWidening` = `GetImageBindableStorageWidening(format)` only when `m_imageBindableStorageRequired`
(`:5810-5813`).
- **`canAppendMipmaps`** `:5815-5827` (append-only level growth) → the `glTexImage2D/3D` append loop
`:5833-5910`.
- Full regeneration `:5912-…`: `ApplyImageBindableStorageWidening` `:5920`; multisample clamp
`:5923-5931`.
- **The per-level upload loop** `:6184-6500` — see §2.7.
- `TextureStorageType::Buffer` `:6504-…`: **calls `BufferImpl::EnsureBufferResource(buffer)` at
`:6520`** (the one place a texture sync pulls a buffer twin — and therefore the one texture path that
reaches `SyncPersistentMappedRange`, see §5), then `CallTexBuffer` / `CallTexBufferRange`
(`:6589-6598`), and mints/points `m_bufferImageSplitViewId` for the image split (`:6606-6630`,
rationale `:6533-6549`).
`SyncTextureViewToBackend` — `Managers.cpp:5579-5670`: syncs the storage owner first (`:5600-5601`,
by-value SharedPtr for the same rehash reason), short-circuits when
`m_viewSourceBackendTextureId == storageBackendTextureId` (`:5613-5616`), otherwise
`RecreateBackendTexture()` (`:5620`, which also bumps `g_attachmentBackendIdGeneration`) and calls
`ResolveTextureViewEntryPoint()` (`:5630-5634`). `StampViewSyncKeys` `:5569-5577`.
### 2.6 Where an attachment-only / image-unit / CopyImage texture gets — or misses — its parameters
**This is the exact answer the task asks for.**
| how the texture is reached | code path | gets `SyncTextureParamsToBackend` + `SyncBuiltinSamplerToBackend`? |
|---|---|---|
| bound to a sampler unit ≤ high-water mark | `SyncNeccessaryTextures` unit list, `DirectGLES.cpp:1898-1900` (hit) / `:1919-1920` → `SyncTextureObjectToBackend` (miss) | **yes** |
| attachment of the **DRAW** framebuffer | `SyncNeccessaryTextures` FBO list, `DirectGLES.cpp:1963-1965` (hit) / `:1974-1975` (rebuild) | **yes** |
| attachment of the **READ** framebuffer only | `SyncCurrentFBO` `:2304-2310` → `SyncToBackend` → `SyncAttachmentObject` `Managers.cpp:7142-7194`, which calls **`SyncMipmapsToBackend` at `:7161` and nothing else** | **NO** — storage is synced, parameters are not. `SyncNeccessaryTextures`'s attachment list reads only `GetFramebufferBindingSlotChecked(FramebufferTarget::Draw)` (`DirectGLES.cpp:1944`) |
| bound to an image unit | `SyncImageTextureBinding` → `SyncTextureObjectToBackend(imageBinding.Texture, /*imageBindable=*/true)` (`DirectGLES.cpp:2071`) | **yes**, and `RequireImageBindableStorage` additionally forces `m_forceTextureParamsResync = true` (`Managers.cpp:4787`) so the swizzle override lands |
| a `glCopyImageSubData` endpoint | `MakeGLESCopyImageEndpoint` → `SyncTextureObjectToBackend(endpoint.Texture)` (`DirectGLES.cpp:7588`); renderbuffer endpoint → `SyncRenderbufferObjectToBackend` (`:7547-7562`, `:7567`) | **yes** |
| `GenerateMipmap` target | `DirectGLES.cpp:7433` / `:7436` → `SyncTextureObjectToBackend` | **yes** |
| `GetTexImage` source | `DirectGLES.cpp:9678` `SyncNeccessaryTextures()` — i.e. only if it is unit-bound or a DRAW attachment; then a bare `Find` at `:9693` | **only incidentally** |
So today parameters ride on the *unit binding* and on the *draw* attachment set. `MGPTextureParams`
(`MGPipeTypes.h:306-318`) is per-`Res` and independent of any view, which is the fix — but the
implementer must not assume the current behaviour is complete: the READ-attachment-only case is a
pre-existing gap that P4a either preserves (bug-for-bug) or closes deliberately, and the choice must be
written down. The corresponding negative control is easy: a texture attached only as READ with a
non-default swizzle / `DEPTH_STENCIL_TEXTURE_MODE`, read back with `glReadPixels`.
### 2.7 Upload shapes — whole box vs rect vs jobs, and the PBO unpack ring
Per dirty (uploadTarget, level) in `Managers.cpp:6185-6499`:
1. Conversion fallbacks first, each of which takes the level **off** the sub-rect path by pointing
`uploadData` at a fresh buffer: `PrepareFallbackUpload` (`:6215-6217`),
`PreparePackedNormUpload` (`:6218-6220`), `PrepareImageWidenedUpload` (`:6225-6227`, defined `:5442`).
2. `subRectEligible` — `:6241-6246`: `uploadData == mipData` (unconverted) **and** a non-empty dirty
region that does not cover the whole level **and** whole-texel byte size **and** the backend upload
size equals the shadow texel size. `bpp = byteSize / texelCount` (`:6247`).
3. Union box (`GetStorageDirtyRegion`, `:6237`) vs **scatter rects** (`GetStorageDirtyRects`, `:6271-6273`,
bounded by `MipmapStorage::kMaxDirtyRects`). **`dirtyRectCount` is forced to 0 whenever the unpack ring
is available** (`:6280-6282`) — the comment records the measured +6 ms/frame Mali cliff from ~100
one-rect jobs; "One box, one job".
4. **Unpack-ring staging plan**, decided once for the level (`:6290-6353`): three shapes matching the
three branches — N rects (`:6310-6322`), one box (`:6323-6331`), whole level verbatim (`:6332-6353`).
`StageBlocksIntoUnpackRing` (`Managers.cpp:4947-5001`, `UnpackStagingBlock` `:4935-4945`) repacks
**tightly**, which is why the ring path issues no `glPixelStorei` at all — the surrounding
`ScopedDefaultUnpackState` already holds ROW_LENGTH/IMAGE_HEIGHT at 0 (`:6296-6302`).
`UnpackRingPixelOffset` `:5003-5006`.
5. `ringStaged` → `BindPixelUnpackBufferId(UnpackRingBufferId())` `:6357-6359`, restored to 0 at
`:6492-6497`.
6. **Stats** `:6360-6395`: `ByteClass::StageTexture`, `CallClass::TextureUploadEmissions`,
`TextureUploadRectEmissions` / `TextureUploadBoxEmissions`, `TextureUploadJobs` (rects → N jobs,
box → 1). *"the box/rect split is counted separately from the bytes on purpose: SSIM is blind to it"*.
7. The dispatch `:6396-6491`: 2D/CubeMap `glTexSubImage2D` in three shapes (`:6399-6431`),
3D/2DArray/CubeMapArray `glTexSubImage3D` in three shapes (`:6438-6485`).
The ring itself is `BufferImpl`'s: `g_unpackRing` (`Managers.cpp:866-872`), `kUnpackRingInitialBytes`
4 MiB / `kUnpackRingMaxBytes` 64 MiB / alignment 64 (`:803-810`), `UnpackRingAvailable`
(`:3466-3469`, honours `MG_Config::Features.EsprytDisableUnpackRing`), `UnpackRingAllocate` (`:3471-3476`),
`UnpackRingMappedPtr`/`BufferId`/`MaxBytes` (`:3478-3480`), `UnpackRingOnPresent` (`:3482`),
`ResetRingForNewContext` (`:2577`). `ScopedDefaultUnpackState` `Managers.cpp:4851-4880`.
Widening helpers: `GetWidenableClientComponentCount` / `IsIntegerWidenableFormat` /
`PrepareChannelWidenedUpload` / `PreparePackedIntWidenedUpload` (`Managers.h:1292-1318`);
`ShouldUseCaveatTextureFormat`, `BackendTextureFormatAddsAlpha`, `GetImageBindableStorageWidening`
(`Utils.h:63`, `:69`, `:130-161`; `Utils.cpp:245`, `:270`, `:280-…`).
Target mapping: `MapToBackendTextureTarget` (`Managers.h:1243-1253`), `GetBackendUploadSize`
(`:1273-1278` — 1D arrays move layers from y to z).
### 2.8 Image units and the format recast
`SyncImageTextureBinding(unit)` — `DirectGLES.cpp:2057-2135`: high-water mark `g_imageUnitHighWaterMark`
(`:2055`), `TrackWritableImageBufferUnit` (`:2062`, counter at `:2040-2047`), unbind path `:2066-2069`,
`SyncTextureObjectToBackend(tex, true)` `:2071`, layered/layer resolution via
`SupportsLayeredImageBinding` (`:2072-2074`, rationale `:1992-1999`), the buffer split view
(`:2120-2124`), and the **bind-format recast** (`:2126-2132`, rationale `:2075-2083`: a `GL_RG32F` bind
is `INVALID_VALUE` on 19/26 non-core formats on Adreno and 25 on both Malis).
`SyncImageTextureBindings()` `:2169-2179`; the per-draw gate
`SyncImageTextureBindingsForDraw(keys)` `:2202-2214`, keyed on `(contextId, samplingGeneration,
g_backendContextGeneration)` with the deliberate note (`:2195-2201`) that the key is the frontend
sampling-resolution generation and **not** the backend re-mint counter.
`MarkWritableImageBufferTexturesGpuWritten()` `:2149-2167`.
### 2.9 Re-mint and the FBO coupling
`RecreateBackendTexture()` — `Managers.cpp:4790-4840`: `ScratchFBOImpl::NoteTextureIdDeleted`
(`:4792`), **`++FramebufferImpl::g_attachmentBackendIdGeneration`** (`:4797`), delete under the matching
context generation (`:4798-4800`), scrub `g_boundTexturesCache` (`:4801-4807`), regen (`:4810`), reset
`m_isInitialized` / `m_backendStorageImmutable` / `m_prevTextureInfo` (`:4818-4820`), reset **every**
parameter cache and set both force flags (`:4829-4839`).
`RequireImageBindableStorage` — `Managers.cpp:4752-4788`: sticky, clears `m_isInitialized`,
**re-marks every already-uploaded level dirty** (`:4769-4779`), sets `m_forceTextureParamsResync`
(`:4787`). `g_attachmentBackendIdGeneration` is defined at `Managers.cpp:7763`.
Destructor `Managers.cpp:4693-4727` (note the `/sdcard/MG/exp_leak_texture_deletes` experiment latch at
`:4711-4718` — an existing temp-experiment hook in the destructor).
### 2.10 CopyImage
`CopyImageSubData` — `DirectGLES.cpp:7696-…`; endpoint resolution `MakeGLESCopyImageEndpoint`
`:7564-7603` (renderbuffer at `:7566-7573`, texture at `:7587-7602`, with the explicit *no* axis remap
for 1D arrays at `:7592-7598`); `GLESCopyImageEndpoint` at `:7530-7542`;
`GetCopyImageEndpointFormat` `:7605-7608`; `CanMirrorCopyImageShadow` `:7613-7617` and the CPU-shadow
mirror below it (`:7619-…`, `:7790` "the CPU shadow is what glGetTexImage answers from, so it has to
follow the same move"). The layered-blit substitute reuses `glCopyImageSubData` at
`DirectGLES.cpp:6288`, `:6406`.
### 2.11 GenerateMipmap
`GenerateMipmap(target)` — `DirectGLES.cpp:7417-7462`.
`EnsureGenerateMipmapStorageAllocated` (`:6707-6752`) for R11F_G11F_B10F / depth-only / RGB16F / RGB32F;
`GenerateThreeChannelFloatMipmapOnCpu` (`:7432-7435`, filtered on the CPU so dirty levels ride down with
the sync); `GenerateDepthTexture2DMipmap` (`:7439`), `GenerateColorTexture2DMipmap` (`:7444`);
`ScopedDetachedTextureFramebufferAttachments` (`:7453`) then `ScopedCompleteFramebufferBinding` (`:7455`)
then `glGenerateMipmap` (`:7460`).
---
## 3. The renderbuffer twin
`RenderbufferImpl::BackendRenderbufferObject` — `Managers.h:2235-2255`. Members:
`m_backendRBOId`, `m_contextGeneration`, `m_isInitialized`, `m_cacheInternalFormat`, `m_cacheWidth`,
`m_cacheHeight`, `m_cacheSamples` (`:2248-2254`).
- ctor `Managers.cpp:10585-10597` (`glGenRenderbuffers`, stamps `g_backendContextGeneration`).
- dtor `Managers.cpp:10599-10612`; no binding shadow exists (`:10606-10607` — `Bind()` always issues).
- `Bind()` `Managers.cpp:10614-10619` — unconditional `glBindRenderbuffer`.
- `SyncToBackend` `Managers.cpp:10621-10693`: **storage respecify is memoised on the four-field cache**
(`:10634-10640`); otherwise `Bind()`, `GenerateRenderbufferFormatInfo` (`:10650`),
`ClampSamplesToBackendSupport` for the multisample form (`:10664-10668`) or
`glRenderbufferStorage` (`:10670-10671`), and an explicit `GL_OUT_OF_MEMORY` probe that reports
`ErrorCode::OutOfMemory` to the application (`:10673-10683`) — the deferred-allocation model means the
error lands on whatever entry point triggered the sync (`:10652-10658`).
- Table `Managers.cpp:10695-10696`. Reached from `SyncAttachmentObject` (`Managers.cpp:7195-7213`),
from `SyncRenderbufferObjectToBackend` (`DirectGLES.cpp:7547-7562`, used by CopyImage), and ticked by
`SyncCurrentFBO` (`DirectGLES.cpp:2235`).
- **Lifetime id / death**: `RenderbufferObject` raises `NotifyStateObjectDestroyed`; the consumer arm is
`Managers.cpp:208-210`.
Frontend members read: `GetInternalFormat` (7 sites), `GetExternalIndex` (4), `GetWidth`, `GetHeight`,
`GetSamples` (2 each).
---
## 4. Readback paths, and what a server without the frontend address space needs
### 4.1 `ReadPixels` — `DirectGLES.cpp:9488-9636`
`IsLegacyNativeReadPixelsFormat` / `…Type` / `IsSupportedDepthStencilReadPixelsPair`
(`:9455-9486`) decide `useNativeReadback`; `GetReadbackChannelMapping` + `GetReadbackDstPixelSize`
decide `convertible` (`:9497-9499`). Then:
`SyncNeccessaryTextures()` `:9507`, `SyncCurrentFBO()` `:9510`, `BindCurrentFBO(Read)` `:9513`,
`ScopedPackState packParamsScope(PackStateFromContext())` `:9516`,
`glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)` `:9518-9524`.
`packSwapBytes` `:9530`, `forceOpaqueAlpha = IsAlphaWidenedFallbackReadAttachment()` `:9533`,
`nativeFastPair` `:9537-9539`; conversion path `:9540-9550`; depth/stencil helpers `:9555-9560+`;
the pack-PBO resolution `GLuint packBufferId` `:9575-9585` (**via
`BufferImpl::EnsureBufferResource(pixelPackBufferObject)` at `:9577`**), `ScopedPixelPackBuffer` `:9589`,
`glReadPixels` `:9593`, the retry-on-error via conversion `:9594-9617`, and the
**PBO → frontend shadow writeback** `:9618-9634` (`WritebackFromBackend` `:9626` +
`BumpBufferMutationEpoch()` `:9628`).
### 4.2 `GetTexImage` — `DirectGLES.cpp:9655-…`
`IsNativeGetTexImagePair` `:9640-9653`; `SyncNeccessaryTextures()` `:9678`; `SyncCurrentFBO()` `:9681`;
resolves the texture off the **active unit's binding slot** (`:9683-9691`) and does a bare
`g_backendTextureObjects.Find` (`:9693-9702`) — it does **not** create a twin, it bails.
Then the cross-FBO resolve barrier note (`:9704-9709`), widening probes at `:9810`, `:9818`,
the temp-FBO + `ReadPixelsViaFormatConversion` route (`:9933`, `:9956`), the pack-PBO resolution
`:10001-10011` and the PBO writeback `:10029-10043`.
**The shadow fallback**: `GetTexImageViaShadowConversion` — `DirectGLES.cpp:9244-9300+`. Valid *only*
while the CPU shadow is authoritative, which holds for non-renderable formats (`:9246-9247`). Reads
`textureMipmapObject->MapMipmapData(uploadTarget, level)` (`:9268`), takes the raw-packed shortcut when
`IsRawPackedPixelTransfer` (`:9277-9287`, so an RGB9_E5 shared exponent is not canonicalised), else
`DecodeShadowDataToWideRGBA` (`:9292-9297`) then `ReadbackImpl::StoreWideRowsToClient`.
`ReadPixelsViaFormatConversion` — `DirectGLES.cpp:9015-9242`; the fixed-point read clamp `:9204-9231`.
`ReadbackImpl` client-store walks: `Utils.cpp:1973-2404`, `StoreWideRowsToClient` `Utils.cpp:2362`,
`StorePackedWordsToClient` `Utils.cpp:2387` (declarations `Utils.h:188-246`); the PBO writeback inside
the shared row walk is `Utils.cpp:2343-2352`.
`StoreReadbackRowsToClient` (depth/stencil destination walk) — `DirectGLES.cpp:8040-8073`, PBO writeback
`:8062`, epoch bump `:8068`.
### 4.3 The scoped driver-state guards (the readback-state-guards architecture)
`DirectGLES.cpp:6545-6644` and `:6795-6900+`:
`ScopedFramebufferBinding` `:6555-6573` (saves/restores through `CurrentFramebufferBinding` +
`BindFramebufferId`, never `glGetIntegerv`), `ScopedPackState` `:6576-6588`, `PackStateFromContext`
`:6592-6596`, `ScopedPixelPackBuffer` `:6601-6607` (always returns the binding to 0),
`ScopedScissorDisable` `:6612-6626` (reads the render-state shadow), `TempFBOBinder` `:6631-6644`,
`ScopedCompleteFramebufferBinding` `:6795-6803`,
`ScopedDetachedTextureFramebufferAttachments` `:6805-…` — **the one direct-iteration site over a twin
table**: push arm uses `g_backendFramebufferObjects.ForEachLive` (`:6877-6884`), legacy arm walks
`begin()/end()` and must test `stateRef.expired()` by hand (`:6885-6895`), pull arm keeps the pre-P2 walk
verbatim (`:6896-6899`). `SlotTables.h:61-68` says the per-entry `weak_ptr` survives **only** for this
caller. `ScopedDepthBlitState` `:6985`.
`PixelStoreImpl` (PACK shadow): `Managers.h:1792-1806`, `Managers.cpp:8004-8054`
(`ApplyPackState` `:8019`).
`ScratchFBOImpl`: `Managers.h:1734-1781`, `Managers.cpp:7767-8002`
(`EnsureId` `:7833`, `EnsureColorAttachment2D` `:7847`, `EnsureCompleteTinyFramebufferId` `:7960`,
`NoteTextureIdDeleted` `:7984`).
### 4.4 What a server without the frontend address space needs
Enumerated so the implementer does not have to re-derive it:
1. **A destination that is not a client pointer.** Every readback today writes either into the
application's `void* pixels` or into a frontend `BufferObject`'s CPU shadow via
`WritebackFromBackend` (four sites, §5). `MGPReadbackInfo` (`MGPipeTypes.h:748-756`) already carries
`DstOffset`/`DstSize` and is `kReplySlot`, so the reply slot is the destination — but the
**pack-PBO** case is different: the driver writes into the *backend* buffer store and the frontend
shadow then has to be refreshed. That refresh is `WritebackFromBackend` + `BumpBufferMutationEpoch`,
both frontend calls, and is P4a's hardest readback item.
2. **PACK pixel-store parameters as payload.** `PackStateFromContext` (`DirectGLES.cpp:6592-6596`) and
the row walks read `MGB_CTX->GetPixelStoreParameters(false)` directly
(`DirectGLES.cpp:9530`, `:8042`, `Utils.cpp:~2300`). `SetPixelPackState`/`MGPPixelPackState` exists
in the catalogue (`PipeCalls.def:126`) — P4a has to actually push it, including the four ES-less
fields (`IMAGE_HEIGHT`, `SKIP_IMAGES`, `SWAP_BYTES`, `LSB_FIRST`; `Managers.h:1788-1791` says they are
honoured on the CPU from frontend state).
3. **The resolved read surface**, not the read-buffer enum: `MGPFramebufferState::ReadSurface`.
The three helpers `IsFixedPointFallbackReadAttachment`, `IsAlphaWidenedFallbackReadAttachment`,
`GetReadColorAttachment` (`Managers.cpp:7280-7408`) all currently walk
`MGB_CTX->GetFramebufferBindingSlot(Read)`; with the inline `InternalFormat` on `MGPSurface`
(`MGPipeTypes.h:344`) they become pure functions of the pushed record.
4. **The texture CPU shadow.** `GetTexImageViaShadowConversion` reads `MapMipmapData`
(`DirectGLES.cpp:9268`) and the CopyImage mirror writes it (`DirectGLES.cpp:~7790`,
`MarkStorageDirty`). A split server has no shadow. Either the shadow stays client-side and the server
answers only from the driver, or a mirror is added — this is the texture analogue of P8's index host
mirror and should be recorded, not solved, in P4a.
5. **Error reporting.** `BackendRenderbufferObject::SyncToBackend` calls `MGB_CTX->RecordError`
(`Managers.cpp:10677-10682`); `GenerateMipmap` calls `RecordGLError` (`DirectGLES.cpp:7461`). Both
need the reverse channel.
---
## 5. `SyncPersistentMappedRange` / `SyncGpuWrites` sites still touching textures
Full census in `MG_Backend/DirectGLES` (the grep is exhaustive):
| site | file:line | texture-related? |
|---|---|---|
| `drawBuffer->SyncPersistentMappedRange()` (indirect) | `DirectGLES.cpp:301`, `:5117-5118`, `:5219-5220` | no (indirect buffers) |
| `indexBuffer->SyncPersistentMappedRange(); SyncGpuWrites()` | `DirectGLES.cpp:4863-4864`, `MultiDraw.cpp:511-512` | no |
| `bufferObject->SyncPersistentMappedRange()` inside the ensure path | `Managers.cpp:2799`, `:2976` (D-N note `:2703-2718`) | **reached from the texture path**: `SyncMipmapsToBackend`'s buffer-texture branch calls `BufferImpl::EnsureBufferResource(buffer)` at `Managers.cpp:6520` |
| `bufferObject->SyncGpuWrites()` | `Managers.cpp:4424`, `:4549` (fp64 narrowing; ID-15 M-3 is the RULED P3a deviation) | no |
| `MarkBufferGpuWritten` / `MarkGpuWritten` for writable image **buffer textures** | `DirectGLES.cpp:2162-2164` (from `MarkWritableImageBufferTexturesGpuWritten`, `:2149-2167`) | **yes — the only texture-driven GPU-write announcement** |
| SSBO / atomic-counter marks | `DirectGLES.cpp:506-510`, `:556-558` | no |
| `WritebackFromBackend` (pack PBO) | `DirectGLES.cpp:8062`, `:9626`, `:10036`, `Utils.cpp:2343` | **yes — these are the readback writebacks**, each paired with `BumpBufferMutationEpoch()` (`:8068`, `:9628`, `:10038`, `Utils.cpp:2351`) |
| `WritebackFromBackend` (XFB capture) | `DirectGLES.cpp:1017`, `:1125` | no |
| `Ops_ReadbackFromGpu` | `Managers.cpp:1726-1761`, tracked wrapper `:1828-1829`, table row `:2300` | no |
So for P4a: **one** texture-owned `SyncPersistentMappedRange` reach (the buffer-texture backing store, via
`EnsureBufferResource`), **one** texture-owned GPU-write mark (writable image buffer textures), and
**four** readback writebacks that are texture/framebuffer-triggered but land on buffers.
`MarkBufferGpuWritten` is `Managers.cpp:2491-2520` (declared `Managers.h:753-763`); the
`BufferImpl::CurrentBufferMutationEpoch` / `BumpBufferMutationEpoch` contract is documented at length in
`Managers.h:558-593` — note it explicitly lists *"every pack-PBO `WritebackFromBackend` site
(glReadPixels/glGetTexImage)"* (`Managers.h:579`) as a bumping path.
---
## 6. Tests and traces
### 6.1 Unit tests (label `unit`)
- **`MG_Test/SanityTest.cpp`** (4378 lines) — the slot-table suite `DirectGLESSlotTable`:
`ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothing` `:3316`,
`RepeatedLookupsOfALiveObjectKeepOneHandle` `:3358`,
`ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire` `:3387`,
`FindNeverMutatesTheTable` `:3416`,
`AWholeTableSavesResetsAndRestores` `:3450`,
`TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin` `:3491`,
`OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind` `:3537`,
`AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep` `:3596`,
`GetOrCreateToleratesANullStateObject` `:3635`,
`AnAnnouncedDeathReturnsTheSlotWithoutASweep` `:3662`,
`EveryReKeyedObjectClassAnnouncesItsOwnDeath` `:3711`,
`TheHandleArmInstallsTheDeathNoticeConsumer` `:3777`,
`TheTwinRegistryCasesInThisBinaryRunOnTheHandleArm` `:3795`,
**`EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm` `:3878`** — drives Texture, Framebuffer,
Renderbuffer, SamplerCso, ShaderCso, VertexElementsCso through
`ExpectTheHandleArmDrivesThisKind` (`:3888-3899`) and then the seventh (Buffer) table by hand
(`:3908-3958`). **This is the case P4a extends** for the kinds it converts to `GetOrCreate(handle)`.
`ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice` `:3963`,
`AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane` `:4004`,
`TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem` `:4061`.
Fixture `ScopedDirectGLESTextureBindings` `:218-260` — **the second live holder of kind Texture**
(saves `g_backendTextureObjects` by value at `:224`, resets with `= {}` at `:228`, restores at `:242`),
used at `:353`, `:404`, `:2699`, `:3065`. `EsprytSlotArmEnvironment` `:90-103`.
- **`MG_Test/Framebuffer/FramebufferTest.cpp`** — 60+ `FramebufferTest.*` cases; the ones P4a's
framebuffer record must keep green: `NamedFramebufferTextureAttachesWithoutChangingBindings` `:244`,
`FramebufferTextureBumpsAttachmentVersionOnlyOnce` `:289`,
`ReadPixelsAllowsPersistentMappedPixelPackBuffer` `:310`,
`NamedFramebufferDrawBuffersDoNotModifyDefaultFramebuffer` `:446`,
`DefaultFramebufferProvidesTextureAttachmentsForFrontAndBackAliases` `:492`,
`FramebufferTexture3DAttachesSliceWithLayerTracking` `:825`,
`ThreeChannelColorAttachmentsAreCompleteThroughTheWidenedSubstitution` `:961`,
`TwoAttachmentCompositeFramebufferMatchesIrisComplementaryPass` `:972`,
**`WidenedDrawBufferIsIdentifiedPerDrawBufferSlotNotPerAttachmentPoint` `:1194`**,
`DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff` `:1227`,
`ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitutesOne` `:1263`,
plus the `PackedReadbackEncodeTest.*` block `:645-769` (pure readback encoders) and
`ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer` `:785`.
- **`MG_Test/Texture/TextureTest.cpp`** — 192 cases. Directly load-bearing for P4a:
`DepthStencilTextureModeIsBackendVisibleThroughTheParamsVersion` `:2609`,
`ShadowReadbackRefusesALayoutItCannotProduceInsteadOfOverrunningTheBuffer` `:1017`,
`ShadowReadbackRefusesAPackStateItCannotHonour` `:1058`,
`GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined` `:1971`,
`GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer` `:2096`,
`CopyImageSubDataRejectsALevelTheTextureDoesNotHave` `:2531`,
`TextureStorage2DMultisampleTracksNamedObjectState` `:1921`,
the border-colour trio `:610`, `:656`, `:698`, and the compressed-upload/PBO block `:2657-2812`.
`MG_Test/Texture/TextureViewTest.cpp`, `MG_Test/Texture/VkClearManagerTest.cpp`.
- `MG_Test/Backend/DirectGLES/` holds only `BaseInstanceInjectionTest.cpp`,
`EsslShaderPassTest.cpp`, `ViewportIndexRoutingTest.cpp` — **there is no backend-side framebuffer or
texture unit test today**; P4a's emitter tests belong in `MG_Test/Pipe/` beside
`ResourceEmitTest.cpp` and `VertexInputEmitTest.cpp` (the P3a shape), which is also where the
`PipeCatalogueTest.cpp` size pins live.
### 6.2 Integration scenarios (label `integration-gpu` / `integration-verify`)
Registered in `MG_IntegrationTest/CMakeLists.txt`; the framebuffer/texture/renderbuffer set with its
case names:
- **`HandleRecycleScenario`** (`:979-1015+`, four+ pinned lanes): `TheReproducerRecyclesEveryName`,
`AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInput`,
`ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents`,
**`ATextureAtARecycledAddressDoesNotInheritItsPredecessorsTwin`** (`Scenarios/HandleRecycleScenario.cpp:869`),
**`AFramebufferAtARecycledAddressDoesNotInheritItsPredecessorsTwin`** (`:920`),
`DestroyedVertexArraysReturnTheirVertexElementsSlots`.
P4a's G8 analogue: add a renderbuffer case and a sampler case here.
- `LayeredAttachmentShapeScenario` (8 cases: `LayeredThreeDColorAttachmentReachesEverySlice`,
`NonLayeredThreeDSliceAttachmentWritesOnlyThatSlice`,
`LayeredCubeMapArrayColorAttachmentReachesEveryLayerFace`,
`LayeredCubeMapArrayDepthAttachmentGatesEveryLayerFace`,
`LayeredCubeMapArrayDepthStencilAttachmentGatesEveryLayerFace`,
`LayeredCubeMapClearMaterialisedBySamplingReachesEveryFace`,
`LayeredOneDArrayClearMaterialisedBySamplingReachesEveryLayer`,
`CubeMapFaceReadbackAnswersTheFaceItWasAskedFor`) — **the direct gate on `MGPSurface`'s
`Layered`/`Level`/`Layer`/`UploadTarget`.**
- `LayeredTextureReadbackScenario` (2), `LayeredAttachmentBarrierScenario`.
- `ThreeChannelAttachmentScenario` (`ThreeChannelColorAttachmentsReportComplete`,
`WidenedAttachmentReadsBackOpaqueWhileItsNeighbourKeepsItsAlpha`,
`DstAlphaBlendingSeesOneInAWidenedAttachment`) — the alpha-widening triple.
- `SnormAttachmentScenario`, `RenderbufferBlendFormatScenario` (3 cases — the only renderbuffer-specific
scenario), `DepthStencilReadbackScenario` + `…MatrixScenario` + `…AttachmentShapeScenario`
(one lane, `TEST_FILTER "DepthStencilReadback*Scenario.*"` at `:639`),
`PackedWordReadbackScenario`, `PixelStoreSweepScenario`,
`ClearThenReadPixelsScenario` (5 cases, incl. `ABlitIntoTheDefaultFramebufferSurvivesAnEarlierClear`
and `AMultisampleResolveBlitIntoTheDefaultFramebufferKeepsItsOrientation`),
`ClearTexImageUndefinedLevelZeroScenario`,
`CopyImageLevelRangeScenario` / `CopyImageLayeredScenario` / `CopyImagePacked16Scenario` (own lane `:806`),
`TextureViewScenario` (8 cases, incl. `BetterCloudsCoveragePipeline` and `CoherencyIsBidirectional`),
`BufferTextureScenario`, `ImageSizeAfterRespecScenario`
(`ADrawSeesTheNewSizeOfARespecifiedImageTexture` — the `RecreateBackendTexture` /
`g_attachmentBackendIdGeneration` gate), `SampledSetStalenessScenario` (2 cases — the
sampling-resolution-generation gate), `ImageTargetKindScenario`, `ImageLoadStoreSsoScenario`,
`NonCoreImageFormatScenario`, `FormatlessImageBakeScenario`, `ImageFormatQualifierScenario`,
`UnboundImageDescriptorScenario`, `IntegerBorderColorScenario` (carries a known ASan
stack-buffer-overflow, ID-21 follow-up), `FragmentOutputArrayIndexScenario`,
`OrientationScenario`, `FragCoordOriginScenario`, `IterationRP*Scenario` ×3, `GuiBatchScenario`.
- `ResourceSubsystemControlScenario.ClearingTheP3aBitsStopsTheEmissionsAndNotThePixels` — the G12
template P4a copies for its own bits; `CsoContentAddressingScenario`; `PipeVerifyArmingScenario`.
### 6.3 Traces most sensitive to this surface
From `tools/trace_replay/trace_cases.json` (40 cases; 79 desktop arms):
- **`improved-transparency-minecraft-26.3`** — the OIT scratch-clear-FBO / draw-buffers-`None` /
read-buffer bug family lives here (`Managers.cpp:7544-7550`). First trace to break on a draw-buffer or
read-buffer regression.
- **`minecraft-1.21.4-fabric-iris-*-in-world`** (17 shader packs: bsl, complementary-reimagined,
complementary-unbound, sundial-lite, iterationt, iterationrp, photon-v1.1/v1.3b, mellow, nostalgia,
bliss, chocapic-v6-lite, super-duper-vanilla, makeup-ultrafast, derivative-main, …) — heavy MRT,
attachment relocation and `glDrawBuffers` permutations; `TwoAttachmentCompositeFramebufferMatchesIrisComplementaryPass`
is the unit-test shadow of this. `iterationrp` has no DirectGLES arm (79 = 40×2 − 1).
- **`minecraft-1.21.4-rd12-odinlite-in-world`** — the VAO/buffer-heavy fixture that cost P3a +30%/+27%
(ID-19/ID-24); also the one that aborts on Xiaomi/Magma pre-existing (ID-23). Texture-upload traffic
is the other half of its frame.
- **`minecraft-1.21.4-fabric-sodium-in-world`** — sub-ms frames, the per-call overhead canary.
- **`minecraft-1.21.1-neoforge-create-instancing-in-world`** (2-frame fixture, not a benchmark) and
**`create-indirect`** (pre-existing `dev` red, BRIEF-P3A.md's first caveat — keep it in the desktop
SSIM corpus, out of the device A/B).
- `minecraft-1.21.4-fabric-{rei,xaero-minimap,xaero-world-map,journeymap,modernui}-*` and
`…-normal-world` — GUI/atlas-heavy, i.e. the `glTexSubImage` union-box vs scatter-rect shape and the
unpack ring; a shape regression there is invisible to SSIM and shows only in
`TextureUploadBoxEmissions` / `TextureUploadRectEmissions` / `TextureUploadJobs`
(`Managers.cpp:6385-6394`).
- `minecraft-1.17-main-menu-854`, `minecraft-1.21.4-fabric-iris-bsl-esc-menu-854`, `OpenRA` — default
framebuffer / blit-to-default paths.
---
## 7. Working notes for the P4a implementer
1. **Seven→ten tables.** P3a converted only the Buffer kind to a call-carried handle
(`Managers.h:690-692`). Framebuffer, Texture, Renderbuffer (and Sampler) still mint inside
`MG_Backend` through `TwinRegistry::GetOrCreate(StatePtr)` (`Managers.h:326-372`) and still depend on
the shared death notice (`Managers.cpp:199-245`). The buffer family's shape — client mints →
`GetOrCreate(handle)` → `ResourceDestroy` retires → **client** frees the slot (`SlotTables.h:337-361`,
`Managers.cpp:233-243`) — is the template. Note the `GetOrCreate(handle)` asymmetry: forward
generations recycle, **backward generations are refused** (`SlotTables.h:301-321`).
2. **`ForEachLive` is the blocker for dropping `Entry::stateRef` on Framebuffer.**
`ScopedDetachedTextureFramebufferAttachments` (`DirectGLES.cpp:6805-6899`) is the only
direct-iteration site and it needs the frontend `FramebufferObject` to read its attachment array. A
handle-keyed FBO table has no `stateRef` (`SlotTables.h:266-269`), so that walk has to be re-expressed
over the applier's `MGPFramebufferState` records before the FBO table can lose its `StatePtr`.
3. **The `Color[8]` / `DrawBuffers[8]` vs 32 mismatch** (§0) has to be ruled on before any emitter is
written; `MAX_COLOR_ATTACHMENT_SLOTS` is 32 at `Managers.h:1579-1581` and `RecomputeBackendColorSlots`
is 32-wide (`Managers.cpp:7443-7448`).
4. **`ContentHash` suppression on `SetFramebufferState`** must not swallow the `fragColorBroadcastCount`
input (§1.6): the broadcast count is read from the frontend *before* the FBO sync precisely so a
program relinks in the same draw. If the FBO record is suppressed, the broadcast count must still be
derivable — either it becomes a field of the record or it stays a separate emission.
5. **`ForceResync` on `MGPTextureParams`** (`MGPipeTypes.h:312-313`) is already the wire spelling of
`m_forceTextureParamsResync`; there is **no** wire spelling of `m_forceSamplerResync`
(`Managers.h:1508-1516`), and the failure mode it guards is not mis-filtering but an *incomplete*
texture that samples `(0,0,0,1)`. That gap is P4a's third contract ruling.
6. **G5-style byte-identity.** P3a's G5 protects eleven buffer-pool/ring functions
(ID-11/ID-13/ID-15). The P4a analogue candidates, if the phase wants one, are the unpack-ring
staging (`StageBlocksIntoUnpackRing`, `Managers.cpp:4947-5001`), `RecomputeBackendColorSlots`
(`Managers.cpp:7432-7519`) and `ScratchFBOImpl`'s attachment shadow (`Managers.cpp:7767-8002`) —
all three are dense, well-tested and have nothing to gain from being rewritten.
7. **Exit order (ID-18/ID-21).** `EnsureProcessTeardownSentinel` / `InProcessTeardown`
(`Managers.cpp:171-180`) gate every twin destructor (`:3556`, `:4693`, `:7026`, `:8085`, `:10432`,
`:10600`), and `OnFrontendStateObjectDestroyed` drops notices during teardown (`:200`). Any new
MGPipe singleton P4a adds must be heap-constructed and intentionally leaked, like the four
`fde5fda3` made never-destroyed.