Files
MobileGL/docs/archive/notes/p4a/scout-espryt-framebuffer.md
T

59 KiB
Raw Blame History

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.