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