Files
MobileGL/docs/archive/notes/p5e/SCOUT-P5E.md
T

15 KiB

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.