mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-15 22:13:24 +09:00
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.
240 lines
20 KiB
Markdown
240 lines
20 KiB
Markdown
# 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`, all `kCtxCso`, therefore already published without a wait
|
||
(`ClientSession.cpp:915-922`). Emitter `VertexInputEmit.h:158-189` (`EmitVertexElements`) and
|
||
`:352-397` (`EmitCreate`, **all 32 attributes and all 32 binding points**, `MGPVertexAttribWire`
|
||
`MGPipeTypes.h:497-508`). Applier record `MGPipeVertexElementsRecord` (`PipeApply.h:389-400`):
|
||
`Attributes[32]`, `BindingPoints[32]`, server-owned `ContentSerial` bumped on every applied
|
||
create, including a re-create on the same handle.
|
||
- `set_vertex_buffers` — `PipeCalls.def:168` (`kCtxState`, `kVarTail`), emitter
|
||
`VertexInputEmit.h:200-247`, record `MGPVertexBuffers` / `MGPVertexBuffer`
|
||
(`MGPipeTypes.h:805-836`). Applier: `VertexBuffers[32]`, `VertexBufferStart/Count`,
|
||
`VertexFetchBaseInstance`, `VertexBuffersSerial` (`PipeApply.h:598-616`), bumped at
|
||
`PipeApply.cpp:2376-2377`.
|
||
- `set_index_buffer` — `PipeCalls.def:169`, emitter `VertexInputEmit.h:255-270`, record
|
||
`MGPIndexBuffer` (`MGPipeTypes.h:839-845`). Applier `IndexBuffer` + `IndexBufferSerial`
|
||
(`PipeApply.h:620-622`), bumped at `PipeApply.cpp:2391-2392`.
|
||
- `set_vertex_attrib_defaults` — `PipeCalls.def:177`, `PipeFill.cpp:2594-2660`; all three views
|
||
verbatim since P5c rv, so `GetCurrentVertexAttribute` is RECORD_SUPPLIED.
|
||
- Client-side dirty rules: bit 8 `NewVertexElements` = `mix(lifetimeId, configVersion)`
|
||
(`Tracker.h:294-298`); bit 9 `NewVertexBuffers` = `mix(mix(AnyVaoAttributeGeneration,
|
||
vaoIdentity), pendingBaseInstance)` (`Tracker.h:542-543`); bit 10 `NewIndexBuffer` =
|
||
`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 own `MGPipeEmitVertexElementsDestroyAndFree`
|
||
(`PipeFill.cpp:1050`) and `NoteRecordDestroyed` (`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 own `g_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 in
|
||
`EmitDrawRecord`'s array arm, gated on the run-ahead caps bit so the lockstep arm is unchanged;
|
||
- **(b) stage**, a new tail on `draw_vbo` shaped exactly like `kDrawHasUserIndices`
|
||
(`MGPipeTypes.h:1430`, codec `PipeWireCodec.cpp:1985-1998`): a per-attribute
|
||
`{BindingIndex, MGHostSpan}` array staged whole from the draw's known `[first, first+count)`
|
||
range. This is P8's `HostResolve.cpp` territory 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` → iterate `i` in `[st.VertexBufferStart, +st.VertexBufferCount)`, skip
|
||
`MGPipeHandleIsNull(st.VertexBuffers[i].Res)`, dedupe on `Res` (a `{slot,gen}` compare replaces
|
||
the pointer compare at `:754-760`), `EnsureBufferResourceForHandle(nullptr, Res)`;
|
||
- `entry.handle = Res` directly — `HandleOfBuffer` at `:769` deleted;
|
||
- dirty repair `:735-736` → `EnsureBufferResourceForHandle(nullptr, entry.handle)`;
|
||
- probe `:733` → `IsBufferDrawCleanByHandle(entry.handle, entry.resource, nullptr)`; the
|
||
`frontend` parameter is already dead under a transport (`Managers.cpp:3148-3150`) and
|
||
`ResolvedDrawBuffers::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
|
||
|
||
1. **Twin key.** `SanityTest.cpp:4083-4084` already pins that `VertexElementsCso` resolves through
|
||
the handle arm. Add: with the twin keyed on `BoundVertexElements`, revert `ResolveVaoTwin` to
|
||
`Find(vao.get())` with the scope at `:1609` removed → `Fatal{RoleViolation, "MGPipeSlots"}`
|
||
naming `HandleOf` (`SlotAllocator.cpp:22-38`). The scope deletion is what makes the revert loud.
|
||
2. **The buffer walk.** New `MG_Test` case beside `VertexInputEmit`: publish `set_vertex_buffers`
|
||
for 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 names
|
||
`st.VertexBuffers`.
|
||
3. **The index arm.** Same shape on `set_index_buffer` / `st.IndexBuffer.Res`, with the frontend
|
||
element slot rebound after publish. Revert 4.3 → red naming `IndexBuffer.Res`.
|
||
4. **The attrib-value memo.** Revert 4.4's key to `GetConfigVersion()` under run-ahead and the
|
||
strict lane aborts `Fatal{UnmigratedPipeInput, "GetBoundVertexArray@draw_vbo"}`
|
||
(`PipeInputs.h:137-170`, `MGPipeStickyForwardPull`) — this is P5e-3's detector doing the work.
|
||
5. **Client arrays.** With §3 G2 (a) in place: a `glDrawArrays` with an enabled client-memory
|
||
attribute under run-ahead is a named refusal, and reverting the refusal is red by
|
||
`DoublePrecisionScenario` / a new client-array scenario under `integration-split-strict`.
|
||
6. **Integration, unchanged suites that must stay green under run-ahead:**
|
||
`VertexAttribBindingScenario`, `VertexArrayEnableDisableScenario`, `PrimitiveRestartScenario`,
|
||
`ResidentIndexScenario`, `MultiDrawScenario`, `DrawParametersScenario`, `DoublePrecisionScenario`,
|
||
`HandleRecycleScenario`, `ObjectSubsystemControlScenario` (the `0x0ff` / `0x17f` A/B at
|
||
`Managers.h:897-905` must 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 `glDrawArrays` and 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 means `SyncClientSideAttributesForDrawArrays`
|
||
cannot 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.** `SyncCurrentVertexAttributeValues` cannot go run-ahead clean on S1's work alone:
|
||
`:1672`, `:1701`, `:1715` read 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_buffers` window: `Count` is "highest ENABLED attribute + 1"
|
||
(`VertexInputEmit.h:203-207`) while `create_vertex_elements` carries all 32
|
||
(`VertexInputEmit.h:352-366`). An attribute enabled in the record but outside the window resolves
|
||
`VertexBufferForAttributeIndex == nullptr` (`Managers.cpp:4795-4809`) and is skipped at `:5262`.
|
||
Unreachable today because both walks read the same `Enabled` bits 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`'s `hostBytes` refresh (`Managers.cpp:3370`) is a
|
||
self-assignment under a transport (`:3217-3220` returns `resource->hostBytes`). Passing `nullptr`
|
||
changes nothing — confirmed by reading, not measured. The fp64 narrowing
|
||
(`SyncFloat64AttributeAsFloat32ByHandle`) reads `resource->hostBytes`, i.e. the server staged
|
||
store, and is already server-owned.
|
||
- **R6.** `g_bufferBackendIdGeneration` (`Managers.cpp:5172-5174`) and the reset rule
|
||
`MGPipeApplierReset` must ADVANCE `VertexBuffersSerial`/`IndexBufferSerial` rather than zero them
|
||
— the long note at `Managers.cpp:5176-5191` says the twin's whole gate depends on it. Under
|
||
run-ahead an `applier_reset` is a waited control record (P5e-2), so this holds; if any package
|
||
makes `applier_reset` fire-and-forget, this gate breaks first.
|
||
- **R7, not settled.** Whether `ResolvedDrawBuffers` should survive at all once the walk is over
|
||
`st.VertexBuffers[Start..+Count)`: that array is already a compact, deduped, handle-keyed list
|
||
bounded by `Count` (1–3 entries on the Minecraft workload, not 32 cold slots). The memo may be
|
||
pure cost after 4.2. Measure before keeping it.
|