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.
20 KiB
S1 — VAO / vertex input / index, under run-ahead
Head 2fde7034. Paths under MobileGL/. Arm assumed throughout: MOBILEGL_PIPE_PUSH with bits 7
and 8 set (ResourceSubsystemEnabled() / VertexInputSubsystemEnabled(), Managers.h:911-918),
EsprytSlotTablesEnabled(), Transport != Monolith. The E note's a.1 is the starting map; the
site set below is re-resolved and differs from it in three places, marked [new].
1. The live reads of this family, at 2fde7034
Legend as the E note's: live = frontend object/array dereference; id = GetLifetimeId() +
a probe of MGPipeSlots(); mint = a write into the client allocator; rec = record arm.
"hit" / "miss" is the memo state of the site's own memo.
| # | site | what it dereferences | kind | when |
|---|---|---|---|---|
| 1 | DirectGLES.cpp:4772 ResolveVaoTwin(currentVAO) → :1617-1618 |
g_backendVertexArrayObjects.Find(vao.get()) → SlotTables.h:411-414 → HandleOf :436-449: stateObj->GetLifetimeId(), then the table's one-entry front memo (:444) or MGPipeSlots().FindByLifetimeId (:445-446); a miss takes GetOrCreate(vao) :215-266, which is MGPipeSlots().Acquire :247-248 |
live + id (mint on a first sight) | every draw; the GetLifetimeId() read runs on the memo hit too |
| 2 | DirectGLES.cpp:4775 |
currentVAO->GetConfigVersion() |
live | every draw, unconditional |
| 3 | SyncVaoAttributeBuffersByHandle memo hit, :733 |
IsBufferDrawCleanByHandle(entry.handle, entry.resource, entry.frontend); under a transport Managers.cpp:3148-3150 does NOT dereference frontend |
none (handle-only) | per distinct VBO, only when memo->vboCleanEpoch != bufferEpoch (:730) |
| 4 | same, dirty repair :735-736 |
currentVAOObject->GetAttribute(entry.attribIndex).Buffer → EnsureBufferResource(obj) → HandleOfBuffer (Managers.cpp:3444, :2922-2941, scope :2938) |
live + id | on a dirty probe |
| 5 | same, memo miss :744-772 |
GetAllAttributes() (32 VertexAttribute, each a SharedPtr<BufferObject> + 10 scalars), attrib.Buffer, EnsureBufferResource(bufferObject) :764, HandleOfBuffer(bufferKey) :769 |
live + id (twice per distinct buffer) | memo key miss |
| 6 | IBO arm :896-925 |
currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject() :897; HandleOfBuffer(possibleIBO.get()) :907; EnsureBufferResource(possibleIBO) :917 |
live + id | every indexed draw; :897 and :907 run on the hit too |
| 7 | SyncCurrentVAO :1641-1652 → SyncToBackend (Managers.cpp:4881-4892) → SyncToBackendFromApplier (:5160-5350) |
nothing frontend — every field is st.BoundVertexElements / rec->Attributes[] / st.VertexBuffers[] / st.IndexBuffer.Res (:5220-5345) |
rec | every draw |
| 8 | SyncCurrentVertexAttributeValues :1662-1719 |
MGB_CTX->GetBoundVertexArray() :1668; program->GetActiveAttributeLocationMask() :1672 (S4); vao->GetConfigVersion() :1683; miss: vao->GetAttribute(location).Enabled :1688; program->GetAttribType(location) :1701,:1715 (S4) |
live | :1668,:1672,:1683 every draw with a program; :1688 on memo miss |
| 9 | :1700 |
MGB_CTX->GetCurrentVertexAttribute(location) |
rec (set_vertex_attrib_defaults, RECORD_SUPPLIED — PipeFill.cpp:2304, :2594-2660) |
pending-mask draws only |
| 10 | DrawArrays :6724-6735 (scope :6729), MultiDrawArrays :6767-6782 (scope :6775) |
g_backendVertexArrayObjects.Find(currentVAO.get()), then SyncClientSideAttributesForDrawArrays(currentVAO, first, count) (Managers.cpp:5364-5468): GetAllAttributes(), and attrib.Offset dereferenced as a raw CLIENT POINTER :5382,:5430 |
live + id + client memory | every glDrawArrays / glMultiDrawArrays with a VAO bound |
| 11 | [new] BoundElementArrayBuffer() :6447-6452 / BoundElementArrayBufferId() :6456-6461, reached from ScopedRestartIndexSubstitution :6591, :6687 |
MGB_CTX->GetBoundVertexArray(), GetIndexBufferBindingSlot().GetBoundObject(), EnsureBufferResource(ibo) → HandleOfBuffer |
live + id | every DrawElements-family call whose ResolveRestartSubstitution (:6464-6480) is not None; also MultiDraw.cpp:45,:905 |
| 12 | [new] :1646 g_backendVertexArrayObjects.CollectGarbageIfNeeded() |
no-op on the slot arm (Managers.h:558-563) |
— | — |
EnsureBufferResourceForHandle (Managers.cpp:3193-3435) holds the SharedPtr but reads nothing
from it under a transport: the shadow base is resource->hostBytes (:3208-3222),
SyncPersistentMappedRange is skipped (:3334-3341), definedness comes from the descriptor plus
staged coverage (:3376-3385). (nullptr, h) is already the live shape at DirectGLES.cpp:953.
Scope sites this family owns: DirectGLES.cpp:1609, :6729, :6775 — 3 of the 20; plus every
HandleOfBuffer reached from rows 4, 5, 6, 11 (Managers.cpp:2938).
2. What the records and the applier already carry
create_vertex_elements/bind_vertex_elements/delete_vertex_elements—PipeCalls.def:151-153, allkCtxCso, therefore already published without a wait (ClientSession.cpp:915-922). EmitterVertexInputEmit.h:158-189(EmitVertexElements) and:352-397(EmitCreate, all 32 attributes and all 32 binding points,MGPVertexAttribWireMGPipeTypes.h:497-508). Applier recordMGPipeVertexElementsRecord(PipeApply.h:389-400):Attributes[32],BindingPoints[32], server-ownedContentSerialbumped on every applied create, including a re-create on the same handle.set_vertex_buffers—PipeCalls.def:168(kCtxState,kVarTail), emitterVertexInputEmit.h:200-247, recordMGPVertexBuffers/MGPVertexBuffer(MGPipeTypes.h:805-836). Applier:VertexBuffers[32],VertexBufferStart/Count,VertexFetchBaseInstance,VertexBuffersSerial(PipeApply.h:598-616), bumped atPipeApply.cpp:2376-2377.set_index_buffer—PipeCalls.def:169, emitterVertexInputEmit.h:255-270, recordMGPIndexBuffer(MGPipeTypes.h:839-845). ApplierIndexBuffer+IndexBufferSerial(PipeApply.h:620-622), bumped atPipeApply.cpp:2391-2392.set_vertex_attrib_defaults—PipeCalls.def:177,PipeFill.cpp:2594-2660; all three views verbatim since P5c rv, soGetCurrentVertexAttributeis RECORD_SUPPLIED.- Client-side dirty rules: bit 8
NewVertexElements=mix(lifetimeId, configVersion)(Tracker.h:294-298); bit 9NewVertexBuffers=mix(mix(AnyVaoAttributeGeneration, vaoIdentity), pendingBaseInstance)(Tracker.h:542-543); bit 10NewIndexBuffer=mix(vaoIdentity, mix(indexSlotVersion, boundLifetimeId))(Tracker.h:558-566). All three emit at the validate point, order elements → buffers → index (PipeFill.cpp:3225-3233). - Twin lifetime, already handle-shaped:
ReleaseTwinsForWireObjectDeath(handle, VertexElementsCso)→g_backendVertexArrayObjects.ReleaseByHandle(handle)(Managers.cpp:352-353), plus the client's ownMGPipeEmitVertexElementsDestroyAndFree(PipeFill.cpp:1050) andNoteRecordDestroyed(VertexInputEmit.h:319-329). SyncToBackendFromApplier(Managers.cpp:5160-5350) is the existence proof: the whole driver-VAO configuration is already produced from records with zero frontend reads, and its dirty gate is three server-owned serials plus the backend's owng_bufferBackendIdGeneration.
So the records cover the family in full except client-memory vertex arrays (§7).
3. The gap — what does not exist yet
G1. Nothing. For rows 1–9 and 11 there is no missing field: every value the server needs is
already in MGPipeApplierState. This family's P5e work is a re-key and a re-source, not a record
addition. That is the headline and it is what makes S1 cheap next to S2/S3/S5.
G2. Client-memory vertex arrays have no wire form. EmitVertexBuffers publishes
Res = kMGPipeNullHandle for them by design (VertexInputEmit.h:196-199, :211-213) and the
SERVER uploads the bytes by dereferencing the client pointer (Managers.cpp:5372-5466). The
brief's "client-side arrays are already refused under split" is NOT confirmed — they are not
refused. EmitTables.cpp refuses only client INDEX arrays for the multi-draw forms
(:651 CLIENT_INDICES) and CLIENT_COMMANDS / UNBOUND_PARAMETER (:682,:685); nothing
refuses a client vertex array. Options, in P5e order:
- (a) refuse,
Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}raised on the GL thread inEmitDrawRecord's array arm, gated on the run-ahead caps bit so the lockstep arm is unchanged; - (b) stage, a new tail on
draw_vboshaped exactly likekDrawHasUserIndices(MGPipeTypes.h:1430, codecPipeWireCodec.cpp:1985-1998): a per-attribute{BindingIndex, MGHostSpan}array staged whole from the draw's known[first, first+count)range. This is P8'sHostResolve.cppterritory and is the honest answer. P5e should take (a) and name (b) as P8's, because (a) is two lines and makes visible a path that was never split-clean; (b) is a record and belongs with the index-array flattening it mirrors.
G3. One pin, not a field. The twin key below is only 1:1 while the vertex-elements CSO stays
IDENTITY-addressed (VertexInputEmit.h:19-28, D-G1). If P7 ever content-addresses it, two frontend
VAOs share a handle — and therefore one driver VAO name and its 64 scratch buffer ids
(Managers.h:1204-1300). P5e must state that as a requirement of the key and pin it in a test, not
leave it as a performance remark.
4. The retirement design
4.1 The twin key is st.BoundVertexElements. The handle ResolveVaoTwin resolves today is
MGPipeSlots().Acquire(VertexElementsCso, vao->GetLifetimeId()) (SlotTables.h:247-248) — the
SAME handle EmitVertexElements mints (VertexInputEmit.h:172) and the applier stores as
BoundVertexElements (PipeApply.h:594). So:
BackendVertexArrayObject* ResolveVaoTwinByHandle() { // split arm, no scope, no allocator
const MGPipeHandle cso = MGPipeApplier().BoundVertexElements;
if (MGPipeHandleIsNull(cso)) return nullptr; // == "no VAO bound"
auto& twin = g_backendVertexArrayObjects.GetOrCreate(cso); // SlotTables.h:294-340
if (!twin) twin = MakeShared<BackendVertexArrayObject>();
return twin.get();
}
PrepareForDraw:4770-4772 stops reading GetBoundVertexArray(); the null arm at :4816-4821
(BindBackendVAOId(0)) is reached from the null handle instead. No behaviour change: one driver VAO
per frontend VAO, exactly as now. GetOrCreate(handle)'s backward-generation refusal
(SlotTables.h:331-339) is the ABA answer, and ReleaseByHandle (SlotTables.h:394-408) the death
answer — both already written and already called from Managers.cpp:352.
4.2 The buffer walk reads the applier, not the VAO. SyncVaoAttributeBuffersByHandle
(:699-790) keeps its shape and loses its frontend argument:
- miss walk
:744-772→ iterateiin[st.VertexBufferStart, +st.VertexBufferCount), skipMGPipeHandleIsNull(st.VertexBuffers[i].Res), dedupe onRes(a{slot,gen}compare replaces the pointer compare at:754-760),EnsureBufferResourceForHandle(nullptr, Res); entry.handle = Resdirectly —HandleOfBufferat:769deleted;- dirty repair
:735-736→EnsureBufferResourceForHandle(nullptr, entry.handle); - probe
:733→IsBufferDrawCleanByHandle(entry.handle, entry.resource, nullptr); thefrontendparameter is already dead under a transport (Managers.cpp:3148-3150) andResolvedDrawBuffers::Entry::frontend(Managers.h:1223) leaves the split arm, which also retires the dangling raw pointer the struct's own note warns about (:1224-1239); attribIndex(Managers.h:1241) becomes the BINDING index, kept only for diagnostics. Memo key is unchanged and already server-owned:{elementsHandle, elementsSerial, buffersSerial}(Managers.h:1263-1265), i.e.{BoundVertexElements, rec->ContentSerial, VertexBuffersSerial}.
4.3 The IBO arm. :896-925 → st.IndexBuffer.Res and st.IndexBufferSerial. The identity
compare memo->iboHandle == iboHandle stays (the handle is now read from the applier, not probed);
the extra HandleOfBuffer at :907 goes. Add iboSerial to the memo so a rebind that moved the
serial re-ensures without a frontend read — the client shutter (Tracker.h:558-566) already fires
on the slot version AND the bound lifetime id, so a redundant rebind is correctly suppressed.
4.4 The two config-version reads. :4775 is DEAD on this arm already (SyncNeccessaryBuffers
uses vaoConfigVersion only in the legacy branch, :831-833): delete it under the transport check.
:1683 is the key of PendingAttribValueMask (Managers.h:1286-1290); re-key it on
{elementsHandle, elementsSerial, activeMask} and read rec->Attributes[location].Enabled
(PipeApply.h:394) at :1688. Every configuration change re-creates the record and bumps
ContentSerial (PipeApply.h:396-399), so the key is strictly stronger than the wrapping Uint16.
4.5 Clean / needs-re-sync, answered from server-owned serials only. After 4.2–4.4 the family's
four questions are: BoundVertexElements + ContentSerial (configuration), VertexBuffersSerial
(the buffer set), IndexBufferSerial (the element slot), CurrentBufferMutationEpoch() +
syncedChangeSerial vs record->Serial (buffer content, Managers.cpp:3147-3153). The epoch is a
backend atomic (Managers.cpp:794) bumped only from backend bodies, i.e. apply-thread memory under
a transport — that must be pinned, not assumed (§7).
4.6 Scopes. DirectGLES.cpp:1609, :6729, :6775 are deleted with the sites they wrap; the
HandleOfBuffer scope entries this family reaches (Managers.cpp:2938) lose their last
vertex-input caller. After that, GetBoundVertexArray (FieldOwnership.def:72) has no Espryt
draw-path reader left except the two client-array arms and the restart substitution (§4.7), so its
row's retiring phase moves from "P8" to "P5e (Espryt)".
4.7 The restart substitution. BoundElementArrayBuffer() (:6447-6452) →
st.IndexBuffer.Res; BoundElementArrayBufferId() (:6456-6461) →
FindBufferResourceForHandle(st.IndexBuffer.Res) (resolve only, exactly as
SyncToBackendFromApplier:5330 does). :6591's read in the substitution body takes the same.
5. Monolith / G1
Every edit above is inside an existing #if MOBILEGL_PIPE_PUSH arm that is already selected by
VertexInputSubsystemEnabled() / ResourceSubsystemEnabled() (Managers.h:911-918) — the
functions touched (SyncVaoAttributeBuffersByHandle, SyncToBackendFromApplier,
BindAttributeBufferByHandle) do not exist in the pull build at all. ResolveVaoTwin is the one
exception: its three arms (:1611-1622 slot, :1623-1636 legacy memo) stay, and the handle arm is
added as a fourth guarded by Transport != Monolith, so MOBILEGL_PIPE_PUSH under
Transport=monolith keeps Find(vao.get()) byte for byte. ResolvedDrawBuffers's frontend and
configVersion members stay declared for the legacy arm (they are already outside the
MOBILEGL_PIPE_PUSH block at Managers.h:1223,:1251), so no mangled name and no struct layout the
pull build sees is changed. G2/G14: no new name enters the pull build's symbol set; the new
ResolveVaoTwinByHandle is MOBILEGL_BUILD_DISAGGREGATED-only.
6. Red-once
- Twin key.
SanityTest.cpp:4083-4084already pins thatVertexElementsCsoresolves through the handle arm. Add: with the twin keyed onBoundVertexElements, revertResolveVaoTwintoFind(vao.get())with the scope at:1609removed →Fatal{RoleViolation, "MGPipeSlots"}namingHandleOf(SlotAllocator.cpp:22-38). The scope deletion is what makes the revert loud. - The buffer walk. New
MG_Testcase besideVertexInputEmit: publishset_vertex_buffersfor buffers A,B, then repoint the frontend VAO's attribute 0 at C WITHOUT re-emitting, then run the sync — the walk must ensure A,B. Revert 4.2 and it ensures C; the case namesst.VertexBuffers. - The index arm. Same shape on
set_index_buffer/st.IndexBuffer.Res, with the frontend element slot rebound after publish. Revert 4.3 → red namingIndexBuffer.Res. - The attrib-value memo. Revert 4.4's key to
GetConfigVersion()under run-ahead and the strict lane abortsFatal{UnmigratedPipeInput, "GetBoundVertexArray@draw_vbo"}(PipeInputs.h:137-170,MGPipeStickyForwardPull) — this is P5e-3's detector doing the work. - Client arrays. With §3 G2 (a) in place: a
glDrawArrayswith an enabled client-memory attribute under run-ahead is a named refusal, and reverting the refusal is red byDoublePrecisionScenario/ a new client-array scenario underintegration-split-strict. - Integration, unchanged suites that must stay green under run-ahead:
VertexAttribBindingScenario,VertexArrayEnableDisableScenario,PrimitiveRestartScenario,ResidentIndexScenario,MultiDrawScenario,DrawParametersScenario,DoublePrecisionScenario,HandleRecycleScenario,ObjectSubsystemControlScenario(the0x0ff/0x17fA/B atManagers.h:897-905must keep its verdicts).
7. Risks and unsettled
- R1 (blocking, owned by S6/S8). Client-memory vertex arrays, §3 G2. Until refused or staged,
the apply thread dereferences a raw client pointer per
glDrawArraysand run-ahead makes the bytes movable. It also GENERATES and fills driver buffers on the apply thread (Managers.cpp:5392-5399,:5455-5457) — harmless, but it meansSyncClientSideAttributesForDrawArrayscannot simply be deleted. - R2.
CurrentBufferMutationEpoch()(Managers.cpp:794,Managers.h's site enumeration) is the only non-serial input left in 4.5. It is an atomic and every bump found is in a backend body, but P5e must pin "no GL-thread bump under a transport" — a client-side bump would make the memo's clean stamp a cross-thread read. - R3.
SyncCurrentVertexAttributeValuescannot go run-ahead clean on S1's work alone::1672,:1701,:1715read the frontend program. S1's edits are a no-op for safety until S4 lands. Sequence S4's program twin before or with this function's edit. - R4. The
set_vertex_bufferswindow:Countis "highest ENABLED attribute + 1" (VertexInputEmit.h:203-207) whilecreate_vertex_elementscarries all 32 (VertexInputEmit.h:352-366). An attribute enabled in the record but outside the window resolvesVertexBufferForAttributeIndex == nullptr(Managers.cpp:4795-4809) and is skipped at:5262. Unreachable today because both walks read the sameEnabledbits in the same validate step; under run-ahead they still do (both are client-side, same step) — but it is the same class as S2's caveat A8 and should be pinned by a test rather than argued. - R5.
EnsureBufferResourceForHandle'shostBytesrefresh (Managers.cpp:3370) is a self-assignment under a transport (:3217-3220returnsresource->hostBytes). Passingnullptrchanges nothing — confirmed by reading, not measured. The fp64 narrowing (SyncFloat64AttributeAsFloat32ByHandle) readsresource->hostBytes, i.e. the server staged store, and is already server-owned. - R6.
g_bufferBackendIdGeneration(Managers.cpp:5172-5174) and the reset ruleMGPipeApplierResetmust ADVANCEVertexBuffersSerial/IndexBufferSerialrather than zero them — the long note atManagers.cpp:5176-5191says the twin's whole gate depends on it. Under run-ahead anapplier_resetis a waited control record (P5e-2), so this holds; if any package makesapplier_resetfire-and-forget, this gate breaks first. - R7, not settled. Whether
ResolvedDrawBuffersshould survive at all once the walk is overst.VertexBuffers[Start..+Count): that array is already a compact, deduped, handle-keyed list bounded byCount(1–3 entries on the Minecraft workload, not 32 cold slots). The memo may be pure cost after 4.2. Measure before keeping it.