mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +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.
193 lines
15 KiB
Markdown
193 lines
15 KiB
Markdown
# P5e scouting brief: retire the lockstep on the Espryt draw path (client runs ahead of apply)
|
|
|
|
Repo (READ-ONLY for scouts): `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`,
|
|
branch `feat/disaggregated`, head `2fde7034` (code head `1f8de61b`). Paths below are under `MobileGL/`
|
|
unless they start with `docs/`. Cite `file:line` at this head; the E note's citations are at `85362a85`
|
|
and may have moved a few lines in `ClientSession.cpp`, `ServerLoop.*`, `PersistentMapTracker.*`,
|
|
`PipeFill.cpp`, `PipeInputs.cpp`, `SlotAllocator.*`, `Doorbell.*` (round 3 touched them) - re-resolve.
|
|
|
|
## Where we are
|
|
|
|
P5d round 3 made `inproc` CPU-bound on the client thread at ~9 ms/frame (Redmi, MC 26.3 VD12, ~850
|
|
draws/frame), apply thread ~2-3 ms of real work per frame plus spinning. The frame is still
|
|
`client work + (per draw) wait for apply + handoff`: every kCtxVerb (draw/clear/blit/dispatch) and
|
|
every reply-slot row waits `appliedSeq == seq` (R-1, `MG_Remote/Client/ClientSession.cpp`
|
|
EmitAndWaitTails). The reason the wait exists is stated in `MG_Remote/CONTRACT-P5C.md` §0 rule E
|
|
and its two named exemptions (§3.1 object-class rows, §5.4 the two scopes): during apply the server
|
|
still dereferences client-owned memory through the 15 object-class BARRIER_PULLED rows of
|
|
`MG_Pipe/FieldOwnership.def` and through the five frontend-keyed twin registries
|
|
(`MG_Backend/DirectGLES/SlotTables.h`, `HandleOf(stateObj)` -> `MGPipeSlots().FindByLifetimeId`,
|
|
`GetOrCreate(const StatePtr&)` -> `MGPipeSlots().Acquire`) inside `MGPipeFrontendKeyedRegistryScope`
|
|
/ `MGPipeReverseAnnouncementScope`. If the client ran ahead, those reads would race the GL thread's
|
|
next verb (bindings move, objects die, the client slot allocator is mutated by glGen/glDelete) and
|
|
the shared `gPipeInputs` block (`MG_Backend/MGPipe/PipeInputs.h`, the residual fill in
|
|
`MG_Impl/Pipe/PipeFill.cpp` MGPipeValidateForVerb step 4, the server stamp in
|
|
`MG_Backend/MGPipe/PipeInputs.cpp`) would be written by both roles.
|
|
|
|
**The research note** `SCOUT-INPUT-E-lockstep-feasibility.md` (same directory as this brief) already
|
|
mapped, per object row, every server-side live read on the DirectGLES inproc draw path (its §(a)),
|
|
why each is unsafe under run-ahead (§(b)), why a snapshot is not the answer (§(c)) and which pushed
|
|
records already cover the data (§(d)). Its conclusion was "not as a P5d round" - the decision now is
|
|
to DO it as its own phase (P5e), because it is the item that lets the two threads overlap.
|
|
|
|
## The target, stated as invariants (what "done" means)
|
|
|
|
P5e-1 On DirectGLES under an active transport, the apply of every op that the client publishes
|
|
without waiting reads NOTHING from client-owned memory: no frontend object dereference, no
|
|
`gPipeInputs` BARRIER_PULLED row, no `MGPipeSlots()` probe or mint. Every object is resolved
|
|
from a handle the record (or the applier state built from records) carries, through a
|
|
handle-keyed twin table.
|
|
P5e-2 The client waits only at: reply-slot rows (already), the control records that need it
|
|
(`applier_reset`, EGL make-current / surface changes), explicit sync (fence client-wait,
|
|
Finish), and a present credit (the client may be at most N presents ahead; N configurable,
|
|
default to be decided by the scouts - 1 or 2). Everything else - draws, clears, blits,
|
|
dispatches, every set/CSO/object record, persistent-map pushes - is fire-and-forget on the
|
|
in-order ring; SEG_CMD / SEG_STAGE space is the only backpressure.
|
|
P5e-3 `gPipeInputs` becomes server-role memory under a live wire: the GL thread does not touch the
|
|
block at all for run-ahead ops (the residual fill and the client-side stamp bookkeeping are
|
|
skipped; the tracker dirty walk and the emitters still run - they produce records). The
|
|
server's stamp + `MGPipeInputUnfreshRead` becomes the detector: any BARRIER_PULLED read
|
|
during a run-ahead apply is `Fatal{UnmigratedPipeInput,...}` (strict), so the strict lane
|
|
(`integration-split-strict`, `MOBILEGL_IPC_STRICT_ERRORS=1`) turns from "expected red" into a
|
|
hard green gate for the migrated verbs.
|
|
P5e-4 DirectVulkan (Magma, P7 not done) keeps the lockstep: the run-ahead switch is a backend
|
|
capability the server publishes (caps snapshot / `CapsMirror`), not a client guess.
|
|
P5e-5 Monolith and the pull build are byte-identical (G1) or every delta is named; the push
|
|
build under `Transport=monolith` unchanged in behaviour; G2/G14 name-set rules kept.
|
|
P5e-6 Evidence: per family a red-once (revert the handle arm -> a named Fatal or a named test red);
|
|
the exit gate is unit + `integration-split` + `integration-split-strict` green for the
|
|
migrated verbs, the four A/B traces rendering under run-ahead on the Redmi with SSIM
|
|
unchanged, and the device numbers (fps + per-thread CPU ms/frame; the client thread
|
|
should stop spending its 30% in WaitForApplied).
|
|
|
|
## Scout assignments (one agent each; read the code, not just the note; write your report file)
|
|
|
|
Every report: (1) the exact live reads of your family at head `2fde7034` (file:line, kind: live /
|
|
id / mint / rec, and whether it runs on a memo hit or only on a miss), (2) what the records and
|
|
applier state ALREADY carry for it (`MG_Pipe/PipeApply.h` MGPipeApplierState, `MG_Pipe/MGPipeTypes.h`
|
|
record PODs, `MG_Pipe/PipeCalls.def` rows, the emitters in `MG_Impl/Pipe/*Emit.h` and
|
|
`MG_Remote/Client/EmitTables.cpp`), (3) the gap: fields/records that do not exist yet and must be
|
|
added (name the record, the fields, the encoding, and the client-side dirty/emit rule), (4) the
|
|
retirement design: the server code shape after the change (which functions change, which twin
|
|
table key, how "is it clean / does it need re-sync" is answered from server-owned serials),
|
|
(5) monolith/G1 impact and how the monolith arm stays byte-identical (`#if`, transport check, or
|
|
a separate function), (6) the red-once test(s), (7) risks and the things you could not settle.
|
|
<= 250 lines, precise, in the voice of the existing contracts.
|
|
|
|
### S1 - VAO / vertex input / index (E note a.1)
|
|
Sites: `DirectGLES/DirectGLES.cpp` ResolveVaoTwin, PrepareForDraw's config-version read,
|
|
SyncVaoAttributeBuffersByHandle (memo hit + miss), the IBO arm, SyncCurrentVertexAttributeValues,
|
|
DrawArrays/MultiDrawArrays client-side attribute paths, `Managers.cpp` EnsureBufferResource(obj) ->
|
|
HandleOfBuffer. State: BoundVertexElements + VertexElementsCsos (content serial), VertexBuffers[]
|
|
{Res,...} + VertexBuffersSerial, IndexBuffer.Res, SetVertexAttribDefaults record. Design question:
|
|
the server's VAO twin key - the vertex-elements CSO handle (one GL VAO per CSO, buffers rebound per
|
|
draw from VertexBuffers[]) or something else; what happens to `ResolvedDrawBuffers` memo keys;
|
|
`EnsureBufferResourceForHandle(nullptr, h)` everywhere; client-side arrays are already refused
|
|
under split (confirm).
|
|
|
|
### S2 - textures and samplers (a.3)
|
|
Sites: CaptureDrawTextureSyncKeys / CurrentUnitBindingsEpoch, SyncNeccessaryTextures (memo hit:
|
|
PairingsIntact + IsDrawSyncClean through borrowed slot pointers; memo miss: unit walk +
|
|
SyncTextureObjectToBackend(frontend)), ResolveAndBindUnitTextures, the sampler pass,
|
|
BindCurrentUnitSamplers, `TextureImpl` twin registry (`StateBackendObjectRegistry<ITextureObject,...>`
|
|
keyed by frontend), `SyncMipmapsToBackend`, `ResolvePushedTextureParams/BuiltinSampler` (P4a/P5c tx
|
|
already read records + the server staged store keyed by handle). State: BoundSamplerViews[unit]
|
|
{View, Texture, Unit} + SamplerViewsSerial, BoundSamplerStates[] + SamplerStatesSerial,
|
|
set_texture_params, tx `StagedTextureStore`. Design: twin keyed by texture handle; per-twin
|
|
"clean" from server-owned serials (params serial, storage/defined-ness serial from tx, sampler-state
|
|
handle) instead of `IsDrawSyncClean(frontend)`; the memo re-keyed on (unit -> texture handle +
|
|
serials); texture death (`object_death` for textures exists since P5c ct?) and the default-texture
|
|
/ builtin-sampler cases.
|
|
|
|
### S3 - framebuffers, attachments, images (a.4, a.5)
|
|
Sites: SyncCurrentFBO/SyncCurrentFBOByRecord, BindCurrentFBO, the draw-FBO texture sync list in
|
|
SyncNeccessaryTextures and SyncReadFramebufferTextureAttachments (identity half = pointer compare,
|
|
`GetAllAttachmentObjects()` live), Clear/BlitFramebuffer bound arms, `g_fboTextureSyncList`,
|
|
SyncImageTextureBinding (Access byte has no encoding), MarkWritableImageBufferTexturesGpuWritten
|
|
(buffer-texture backing buffer via frontend). State: FramebufferRecords[handle] {Color[8]/Depth/
|
|
Stencil surfaces, DrawBuffers, IsDefault}, BoundFramebuffer[2], FramebufferSerial, BoundShaderImages[]
|
|
{Res, Unit, InternalFormat, Layer, Level, Layered, Access}, the TexBuffer backing record (P4a).
|
|
Design: framebuffer twin keyed by handle (already? `FindByHandle(record.Fbo)` exists), attachment
|
|
lists from the record's surface handles, the image `Access` encoding, the buffer-texture backing
|
|
handle on the server.
|
|
|
|
### S4 - programs / shader state (a.2)
|
|
Sites: SyncCurrentProgram, BindCurrentTextures (program-keyed memo), BindCurrentProgramWithResources
|
|
(link/spirv status, UBO size, MapUBO without ShaderCso, GetUniformBlockBinding, backend state
|
|
version, sampler-pass uniform unit indices), SyncAtomicCounterBuffers, PrepareForCompute's
|
|
GetProgramForDispatch, `g_backendProgramObjects` registry, GetProgramObject/ValidateProgramName
|
|
sticky rows. State: DrawProgram / DispatchProgram / BoundShaderCso handles, ShaderCso records
|
|
(create_shader_state: SPIR-V + full reflection = `ProgramState/ProgramArtifacts.h`), GlobalConstants
|
|
bytes + version. Design: program twin keyed by the shader-state handle; every reflection question
|
|
answered from the archived artifacts; the "no ShaderCso record" arm (MapUBO) - who takes it and
|
|
whether it can be refused under run-ahead; link status as record state.
|
|
|
|
### S5 - buffer binding points and the remaining rows (a.6 + the tail)
|
|
Sites: SyncBufferBindingPoints (UBO/SSBO/atomic/XFB), the UBO loop in BindCurrentProgramWithResources,
|
|
MarkShaderStorageBuffersGpuWritten, SyncAtomicCounterBuffers, GetBufferBindingPointCount sticky,
|
|
XFB rows (GetTransformFeedbackProgram, HasOpenTransformFeedbackSpan, StartPendingTransformFeedback),
|
|
GetBufferBindingSlot (15 pointers, filled every draw, no draw-path reader), RecordError.
|
|
State: `set_shader_buffers` (`MGPShaderBuffers`, PipeCalls.def:174, kCtxState, kVarTail|kHostSpan,
|
|
CONTRACT-P5C.md:169-176 says catalogued but NOT emitted until P4b). Design: the emission (client
|
|
tracker dirty rule for the four binding-point tables - what dirty bit exists, `gen_pipe_dirty_surface`
|
|
implications), the applier-side table, the Espryt consumers (UBO points at draw, SSBO/atomic points,
|
|
XFB capture points), the named-UBO host span (D-B8 is Magma-only - can the Espryt emission omit
|
|
it), `GetBufferBindingPointCount` from the record; what stays lockstep (an open XFB span? dispatch
|
|
with atomic counters?) and how the client knows to wait there.
|
|
|
|
### S6 - transport mechanics of run-ahead (fable)
|
|
`MG_Remote/Client/ClientSession.cpp` EmitAndWaitTails / the barrier flags / DrainEventRing /
|
|
AwaitBufferWriteback / Stop's drain, `MG_Remote/Transport/SessionRings.h` (appliedSeq, presentAck,
|
|
WaitForCmdSpace, stage retirement), `MG_Remote/Server/ServerLoop.cpp` + `PipeApplier.cpp`
|
|
(ApplyOne, StampVerbBoundary, LeaveApplier, the deferred-destroy queue from P5d round 1 in
|
|
`MG_Pipe/PipeMutation.h`), `MG_Remote/Client/PersistentMapTracker.cpp` (pushes before verbs),
|
|
`MG_Remote/Client/GpuWritePending.cpp`, `CapsMirror.*`, `ConfigLoader.cpp` IPC knobs,
|
|
`docs/Disaggregated/ARCHITECTURE.md` §11 (reverse channel, present credit `MOBILEGL_IPC_PRESENT_CREDIT`
|
|
mentioned as P6+/P10), §17. Design: (1) the exact wait rule per op under run-ahead and the switch
|
|
(`MOBILEGL_IPC_RUN_AHEAD`? default, the caps bit Espryt publishes, Magma stays lockstep, the
|
|
negative control), (2) present pacing: `WaitForPresentAck` with credit N; what N does to the two rings
|
|
(SEG_CMD 8 MiB, SEG_STAGE 32/256 MiB) at ~850 draws + 0.36 MB pmap per frame, (3) where events are
|
|
drained now (present, reply rows) and which client operations must force a drain/wait
|
|
(MapBuffer readback, GetBufferSubData, glFinish, fence client-wait, glGetError semantics under
|
|
run-ahead - name the relaxation), (4) `gPipeInputs` ownership: the client-side skip of the residual
|
|
fill + stamp bookkeeping under a live wire (which of `MGPipeValidateForVerb`'s steps stay: the
|
|
tracker walk and the emitters must), the server-only stamp, the strict detector, the verify build
|
|
(`MOBILEGL_PIPE_VERIFY` forces lockstep - confirm), (5) object lifetime under run-ahead: slot reuse
|
|
{slot,gen} vs records still in flight, `object_death` ordering, the deferred-destroy queue, the
|
|
client memos that hold frontend->handle maps, `Forget()` of persistent maps while pushes are in
|
|
flight in SEG_STAGE, (6) the two scopes and `MGPipeRefuseAllocatorFromApplyThread`: after P5e the
|
|
apply thread must never enter them - the guard becomes unconditional Fatal; list every remaining
|
|
apply-thread allocator touch that the other scouts' designs do not already retire (readback paths,
|
|
CopyTex, mip, blit named arms - are they run-ahead ops or waited ops?), (7) the measurement plan on
|
|
the Redmi (bench scripts exist: `~/w7/notes/tools/p5d_bench_{fcl4,all4}.sh`).
|
|
|
|
### S7 - identity: the five twin registries and the allocator (fable)
|
|
`DirectGLES/SlotTables.h` (BackendSlotTable, StateBackendObjectRegistry, HandleOf, FindByHandle,
|
|
GetOrCreate(handle) from P5c hd, the holder list / DestroyByLifetimeId, dead-generation rules),
|
|
`Managers.cpp` HandleOfBuffer / g_backendBufferResources / OnFrontendStateObjectDestroyed /
|
|
EmitObjectDeathRecord (P5c ct), `MG_Impl/Pipe/SlotAllocator.*`, `MG_Impl/Pipe/ResourceTracker.h`,
|
|
the client-side lifetime-id -> handle maps, `MG_Pipe/PipeMutation.h` deferred destroys. Deliver:
|
|
for each of the five kinds (buffer, VAO, texture, framebuffer, program/shader state) + sampler +
|
|
renderbuffer if they have twins: today's key, the record that carries the handle at create/bind,
|
|
whether a death record exists, and the exact change to make the registry handle-keyed with no
|
|
`GetOrCreate(const StatePtr&)` / `HandleOf(stateObj)` reachable on the apply thread; the twin
|
|
creation moment (first record naming the handle) and the twin death moment (object_death / applier
|
|
reset); the "recycled slot, new gen" ABA answer; how the 20 scope sites are removed and the two
|
|
scopes deleted (or reduced to monolith-only). This is the spine the other scouts hang on - be exact.
|
|
|
|
### S8 - synthesis (fable; runs after S1-S7)
|
|
Read all seven reports and write `CONTRACT-P5E-draft.md` (the rules: run-ahead op classes and
|
|
wait points; server-owned `gPipeInputs`; handle-keyed twins; the new records/fields; Magma stays
|
|
lockstep; the strict gate) and `BRIEF-P5E.md`: packages with file ownership boundaries that can be
|
|
implemented in parallel worktrees, their order/dependencies (which package lands first so the
|
|
others build on it - likely S7's registry re-key and S5's record), per-package red-once and quick
|
|
gate, the integration gate, the device exit, and the open questions that need the integrator's
|
|
ruling. Name conflicts between scout designs explicitly and resolve them (or list them as rulings
|
|
needed). <= 500 lines each; write the files before the final message.
|
|
|
|
## Rules
|
|
Read-only: no edits to the repo, no builds, no adb. Reports go to
|
|
`C:/Users/geekerwan/AppData/Local/Temp/claude/C--Users-geekerwan-AndroidStudioProjects-FoldCraftLauncher-MobileGL-disagg/5007453d-cb19-4ebf-96a3-60ba4fb55b47/scratchpad/p5e/scout-<id>.md`
|
|
(write the file BEFORE the final message; the final message is the structured result only).
|
|
No process narration in the reports. Cite at head `2fde7034`.
|