Commit Graph
3153 Commits
Author SHA1 Message Date
swung0x48 44f91c7405 [MG_Backend, MG_Test] (Disaggregated): P5e wave 2 integration - the three seams where two packages met
- vi and sb both added a block at the same point of DirectGLES.cpp's buffer
  section; both belong, and vi's arm used to borrow the `#endif` that follows the
  hunk, so it gets one of its own and sb's block opens below it.
- pg and sb both rewrote the UBO loop's first lines: pg changed where `binding`
  comes from (the archived block-binding tail, because glUniformBlockBinding moves
  it after the link), sb changed what `binding` indexes (the applier's window
  instead of the frontend binding-point array). The merge keeps pg's source and
  sb's consumer, which is the boundary both reports named.
- pg wrote a comment stating the shape of the texture memo key because the line
  belongs to tx2; tx2 wrote the fields. Both kept.
- PipeCatalogue's routed-row pin read 38 at the base, and sb (one generated row)
  and pg (one escape) each landed 39 against it. The merged tree has 40, which is
  what the count now asserts, with both rows named in the comment.
- Gates on the merged tree: unit 2250/2250, integration-split 113/113, gen_pipe
  and gen_pipe_field_ownership --check/--self-test green, include closure 4 probes
  0 problems, dirty surface --check/--self-test green, doc citations one
  pre-existing CONTRACT-P5.md ambiguity. Strict is still the phase's two-sided
  lane; its first-blocker markers fell from {GetTextureUnitObject@Clear 13,
  GetFramebufferBindingSlot@Clear 6, GetTextureUnitObject@GenerateMipmap 2} to
  {GetFramebufferBindingSlot@Clear 2, GetProgramForDraw@DrawArrays 1} plus the
  allowlisted barriered rows (ReadPixels, CopyTexImage2D) that were always behind
  them. Run-ahead is still OFF: kMGPipeP5eRunAheadReady and
  kMGPipeP5eClientWaitRuleLanded are both false, so this is the lockstep split
  with every family reading records instead of the frontend.
2026-09-18 08:45:20 -04:00
swung0x48 e7a66d2f24 Merge branch 'p5e/ra' into feat/disaggregated (P5e ra) 2026-09-18 08:41:10 -04:00
swung0x48 cae68992b0 Merge branch 'p5e/fb' into feat/disaggregated (P5e fb) 2026-09-18 08:41:10 -04:00
swung0x48 db2196fb08 Merge branch 'p5e/tx2' into feat/disaggregated (P5e tx2)
# Conflicts:
#	MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
2026-09-18 08:41:10 -04:00
swung0x48 30e385812d Merge branch 'p5e/pg' into feat/disaggregated (P5e pg)
# Conflicts:
#	MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
#	MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp
2026-09-18 08:40:39 -04:00
swung0x48 5b226f16ae Merge branch 'p5e/sb' into feat/disaggregated (P5e sb)
# Conflicts:
#	MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
2026-09-18 08:39:37 -04:00
swung0x48 e0f69e8791 Merge branch 'p5e/vi' into feat/disaggregated (P5e vi) 2026-09-18 08:38:14 -04:00
swung0x48 4d1499ef7e [MG_Test] (Disaggregated): P5e pg - four cases for the bindings record, the failed relink and the framed archive
- AUniformBlockBindingAfterTheLinkTravelsOnItsOwnRecord: the create unlatches the
  bindings and the first emission after it always goes out (the ordering the applier
  depends on), an unchanged binding set emits nothing, and a post-link unit assignment
  moves the latch. This is the case the package's first red-once is built on: the
  integration-split lane has no scenario with a named uniform block bound to a non-zero
  point, so emptying the tail there is invisible and the statement has to be made here.
- TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem: whole-set replacement in
  all three tails, the override NAME copied rather than pointed at (rule C), BindingsSerial
  advancing, and the re-issued create dropping every tail because they index into the
  archive it just replaced.
- AFailedRelinkReIssuesWithLinkStatusZeroAndNeverAnObjectDeath: ID-88's "iff the frontend
  reports it unlinked" is asserted against the frontend rather than assumed, so the day
  Link()'s prologue stops withdrawing LINK_STATUS this case says the ruling has to be
  re-decided instead of the server silently drawing a dead executable.
- TheFramedArchiveRoundTripsWithTheStageOfEveryModule: two modules of the SAME stage -
  the shape MGPProgramDesc::StageMask cannot describe - survive the frame, the skipped
  member comes back null, and a truncated frame is refused with both outputs defined.
2026-09-18 08:30:19 -04:00
swung0x48 ca1beb6958 [MG_Backend] (Disaggregated): P5e fb - revert the tx2 scaffold; the seam goes back to c0e's stub
- Reverts f4675d1a. tx2 owns TextureImpl::SyncTextureToBackendByHandle and
  BackendTextureObject::SyncMipmapsToBackendByHandle; fb bodied them temporarily only so
  that fb's own integration-split and strict lanes could be measured at all, and this
  puts the c0e stubs back so the two packages cannot collide at the merge.
- WHAT THIS COSTS, AND IT IS SAID HERE RATHER THAN DISCOVERED: with the stubs back,
  every integration-split entry whose framebuffer has a TEXTURE attachment - or whose
  draw binds an image - aborts on
  Fatal{UnmigratedVerb, "TextureImpl::SyncTextureToBackendByHandle"} rather than on
  anything fb wrote. fb's lane is green the moment tx2 lands beside it; the numbers
  measured with the scaffold are in the package report, with the scaffold's own cost
  (a GetTextureObject pull per call) named.
2026-09-18 08:30:18 -04:00
swung0x48 48b2f56ef8 [MG_Remote] (Disaggregated): P5e ra - the run-ahead latch goes down with the session
- CONTRACT-P5E §1. The latch means "what THIS session's first caps adoption said", and
  ClientSession is a process singleton that Start/Stop can cycle. Left standing, a second
  Start would inherit a verdict about a server that is gone - and because a later
  snapshot may only turn run-ahead OFF, an inherited `true` could never be corrected by
  the honest answer arriving afterwards.
- The present serial resets with it: a credit's id space belongs to the session that
  paces on it, and a server that never saw frame 1 cannot return its credit.
2026-09-18 08:27:00 -04:00
swung0x48 1c8727102c [MG_Backend] (Disaggregated): P5e tx2 - name the two entries that actually stamp the sync handle
- ResolveTextureTwin is the lookup WITHOUT the sync and does not stamp it; the comment said it did.
2026-09-18 08:26:09 -04:00
swung0x48 6ee4c0979b [CI, Docs] (Disaggregated): P5e ra - the strict lane becomes one step with two arms, and §11/§14/§17 say what landed
- CONTRACT-P5E §7. integration-split-strict was an EXPECTED-RED control: every entry
  aborts on its first BARRIER-PULLED read, so the step gated on the run failing for
  that reason. After the flip it is a HARD GREEN GATE - an unbarriered record's pull is
  Fatal whatever the knob says (§3.3), so strict adds only the barriered rows, and those
  are a short named allowlist of <field>@<verb>. A revert of any handle arm in
  vi/sb/pg/tx2/fb then goes red HERE, by field and verb, instead of in a picture.
- THE ARM IS CHOSEN BY THE TREE. The step greps kMGPipeP5eRunAheadReady out of
  MG_Backend/Init.cpp, which is the one constant the integration commit flips and is
  exactly the fact the lane's expectation depends on. A lane whose flip is a separate
  human step is a lane that is wrong for as long as that step is outstanding - and the
  pre-flip arm is unchanged, so this commit moves no CI verdict today.
- The allowlist names the package that retires each row: readbacks and the pack PBO
  (P8/P9), the CopyTex endpoint and GenerateMipmap (tx2), set_storage_block_binding
  (pg), XFB (P3b/P4b/P9), resource_copy_region's GL names (the declared P5b debt). A
  row leaves it by deletion, never by widening. The lane also warns on a non-zero rsp,
  which after P5e can only be a barriered row.
- The allowlist is built with printf rather than a heredoc: a quoted heredoc's
  terminator has to start at column 0, which inside a YAML block scalar would end the
  scalar instead of the heredoc.
- ARCHITECTURE §11.6: the wait rule is the generated WaitClass column now, not the call
  class, and it REPLACES the BatchWaits skip; glFinish stops being a no-op under
  run-ahead; the forced waits (EGL forwarders, the map/readback family) and glGetError's
  named window are listed. §11.7: the P9 plan is code - the producer blocks, the drain
  clears AND rings, the client's parks break on the latch, and two named refusals remain
  for the impossible cases. §14: the present credit is client-minted, 1-based, one per
  swap, paid before the encode. §17.7: what P5e is, in one section - the predicate, rule
  F, gPipeInputs' ownership, the latch and the control arms.
- 附 A gains MOBILEGL_IPC_RUN_AHEAD and moves MOBILEGL_IPC_PRESENT_CREDIT out of the
  "P6+" line: a knob listed as not-yet-effective while it paces the frame is how an A/B
  stops being one.
2026-09-18 08:22:49 -04:00
swung0x48 b004e5dcbb [MG_Backend] (Disaggregated): P5e sb - Espryt's four binding-point walks read the record
- SyncBufferBindingPointsByRecord(class, glTarget), file-local beside the walk it replaces:
  the window is the applier's ShaderBufferStart/Count, each entry resolves through
  EnsureBufferResourceForHandle(nullptr, Res), and whole-vs-range is
  `Offset == 0 && Size == kMGPipeWholeBuffer` - so obj->GetSize() leaves the function and the
  extent of a base binding is re-resolved HERE, against this server's own storage, which is
  what makes a glBufferData between the emission and the apply bind the new extent as GL does.
  The ES clamp stays exactly where it was: the device ceiling is a server question.
- The arm is transport + bit 13, with bit 13 requiring bit 7 refused by name rather than
  half-run - ResolveVertexInputSubsystemArm's shape, and for its reason: without bit 7 every
  point would log once and `continue` WITHOUT unbinding.
- SyncAtomicCounterBuffers reads the applier's window instead of
  GetBufferBindingPointCount's sticky forward; glBindings stays the backend program's own
  list. A binding outside the window takes the UNBIND arm rather than being skipped, because
  skipping would leave whatever the driver last had at that ESSL point.
- The UBO loop's POINT half reads BoundShaderBuffers[Uniform][binding] in the same index
  space, with IsBufferDrawCleanByHandle / FindBufferResourceForHandle in place of the
  frontend probes. GetUniformBlockBinding above it is untouched - that line is package pg's.
- MarkShaderStorageBuffersGpuWritten and SyncAtomicCounterBuffers' MarkBufferGpuWritten are
  DELETED under a transport. They walked client memory and wrote into a client object, which
  is the one thing that cannot survive run-ahead, and the client's own MarkGpuWritesForDraw
  has covered the same set since P5d. Withholding the client's two rows turns the atomic
  scenarios red (5 vs 13, 100 vs 116), which is what says the duplicate really is gone.
- Every frontend arm is kept token for token under Transport=monolith (G1, ruling 1), as the
  `if (…) else` the indirect buffer already uses.
2026-09-18 08:22:45 -04:00
swung0x48 bbfd4fef81 [MG_Test, MG_Impl, MG_Pipe] (Disaggregated): P5e ra - the wait rule's cases, and the two constants are one switch
- Fifteen cases, and every one of them arms run-ahead through the production latch
  (publish kCapRunAheadApply into the caps mirror before Start) rather than through a
  test-only setter - ID-102's lesson: a red-once whose probe is a branch beside the
  production path is a red-once that proves the branch.
- RemoteRunAhead: the knob alone does not arm it; VERB_BARRIER=0 disarms it even with
  the cap bit; a credit-1 client pays at every present after the first and a credit-3
  client at none of its first three; an unbarriered verb fills nothing and is allowed;
  a 3 MB event burst behind an unwaited sequence flow-controls instead of aborting and
  arrives LOSSLESS; only the texture half of resource_subdata wants its reply.
- RemoteGuards: a gPipeInputs touch outside a barriered fill under run-ahead is
  Fatal{RoleViolation, "gPipeInputs"} by name and the barriered fill is allowed; a
  residual pull under an UNBARRIERED record is Fatal WITHOUT the strict knob while the
  same pull under a barriered one still only counts (ruling 4, in executable form); a
  deferred destroy from an unbarriered apply is the finding and from a barriered one is
  still served (ruling 13).
- SessionTest: a full SEG_EVENT wakes a client parked on appliedSeq AND one parked on
  the present credit, without touching either watermark; the drain clears the latch and
  RINGS, once, and is silent when the latch was already down.
- The event burst's drainer starts 50 ms late on purpose. A drainer that starts at once
  can keep a 256 KiB ring from ever being full, and a red-once that depends on the
  scheduler losing a race is not a red-once.
- ClientVerbIsBarriered now takes a nullable context: the validate point asks the
  question before it knows whether there is anything to fill, so the answer is about the
  RECORD. A null context has no open XFB span, and treating it as "barriered" instead
  made the fill red-once unreachable in any fixture without a live GLContext.
- kMGPipeP5eClientWaitRuleLanded flips WITH kMGPipeP5eRunAheadReady, in the integration
  commit - an amendment to ID-103's wording ("the same commit that lands the wait rule").
  The argument ID-103 makes is about whether the CLIENT is parked: RunAheadArmed() needs
  the server's caps bit, so between ra landing and the bit being published the client
  still blocks after every record, and a `false` stamp there would withdraw §4.4's
  exemptions from probes that wait still makes safe. Two spellings, one switch.
2026-09-18 08:22:26 -04:00
swung0x48 99211a4807 [MG_Test] (Disaggregated): P5e tx2 - the ABA case asks the twin table, not the record
- MGPipeApplyResourceCreate declines a TEXTURE create on a process with no P4a consumer
  (NoP4aConsumer), which is every unit-lane process, so the record-driven shape of this
  case skipped instead of running. The ABA answer is the twin TABLE's - a forward
  generation resets the entry - so the case now drives g_backendTextureObjects directly
  and gates on EsprytSlotTablesEnabled() rather than on the family bit.
- And it does NOT compare the successor's twin ADDRESS against the predecessor's: the
  reset released the predecessor, so the allocator hands the successor the same heap
  address, which is precisely why an address is never an identity here. "The slot came
  back empty" is the assertion.
2026-09-18 08:21:48 -04:00
swung0x48 aa38f0a05b [MG_Test] (Disaggregated): P5e fb - the three constants on both sides, the server's point set, and the resize that must re-emit
- ImageEmit.TheThreeAccessConstantsArePinnedOnBothSidesOfTheWire: ruling 16 / ID-94 is
  that the encode and the decode are ONE table, so the case reaches both halves - the
  numbers, MGPipeEncodeImageAccess, MGPipeDecodeImageAccess, the validity predicate and
  the two read/write predicates - and then round-trips all three GL tokens through a
  real glBindImageTexture so what is pinned is the byte that actually crosses. The
  drift it catches (1 <-> 2) is a `writeonly` image the shader can read back, which no
  pixel test on a lockstep tree could see while the frontend value was answering.
- FramebufferEmit.TheRecordsElevenSurfacesAreTheWholePointSetTheServerReads: the server
  twin of AnAttachmentPointAboveTheWireWidthIsRefusedNotTruncated. P5e spends that
  refusal as a licence - the by-handle attachment walk iterates Color[0..7] + Depth +
  Stencil instead of the frontend's 41 points - so both halves are pinned: a
  fully-populated framebuffer round-trips every point into the applier's record, and a
  point above the width leaves the record the server holds completely unmoved (hash and
  FramebufferSerial), rather than merely counting a refusal.
- FramebufferEmit.ADefaultFramebufferResizeReEmitsTheRecordWithItsNewExtent: ruling 15,
  which S3 flagged as unsettled and said was load-bearing because under run-ahead there
  is no other extent source. The case drives the surface-changed consumer's own call -
  AllocateStorage on the attachment at the new extent, no rebind, no re-attach - which
  is exactly the shape the framebuffer shutter would have suppressed.
- All three get their pull-build skip twin through the suites' X-macro lists (G2/G14).
2026-09-18 08:14:46 -04:00
swung0x48 c5491ca675 [MG_Backend] (Disaggregated): P5e pg - the program twin is keyed by the ShaderCso handle
- BackendProgramObjectImpl::SyncToBackendByHandle is real. It and the frontend overload
  feed ONE body (SyncToBackendFromSource) through ProgramArchiveSource, a view whose
  accessors are ProgramObject's names one for one - so the body is written once and the
  PULL build compiles the same text against a ProgramObject. In a pull build
  ProgramBuildSource IS ProgramObject, the function keeps its own name and its null
  guard, and G1's byte identity survives a rename and nothing else.
- The view answers the three POST-LINK MUTABLE reflection fields from the record's
  set_program_bindings tails and everything else from the record's archive. The tails are
  a whole-set replacement, so a location the sampler tail does not name has NO unit - not
  whatever the link-time snapshot said - and "has a bindings record been applied at all"
  is BindingsSerial != 0 rather than a non-empty tail.
- The nine-clause clean condition keeps its clause count and moves its inputs (D-H5):
  record.Serial vs the m_syncedShaderCsoSerial stamp P4a landed and nothing has read
  until now, BindingsSerial vs a new m_syncedBindingsSerial (a glUniform1i on an IMAGE
  uniform is baked into the ESSL, so the record that carries it must force a rebuild),
  record.Signature, Desc.LinkStatus and Desc.SpirvStatus.
- The signature has ONE authority per arm: the handle arm stamps and compares the
  CLIENT's number verbatim, so the two commutative hashes never have to stay
  bit-identical for a program with an override to stop rebuilding every draw.
- DirectGLES: SyncCurrentProgramByHandle, the stash re-keyed on the handle,
  PrepareForDraw/PrepareForCompute reading MGPipeApplier().DrawProgram/DispatchProgram,
  BindCurrentProgramWithResources and GetCurrentBackendProgram on the handle arm, the
  block binding and the sampler unit from the record's tails, and the sampler-pass memo
  on BindingsSerial instead of the frontend's backend-state version.
- MapUBO() has no run-ahead answer and now says so: every decline of
  ResolveGlobalConstantsRecord under a transport is Fatal{UnmigratedVerb,
  "set_global_constants"} rather than a torn read of the client's live globalUboScratch.
- The fragColor broadcast count takes a POLICY, because a dispatch is a third case: it
  has no draw framebuffer and no fragment stage, PrepareForCompute runs no SyncCurrentFBO,
  so it may neither fall back to a client row nor abort when the records are untrusted.
- Three of the family's five named frontend-keyed scope sites are now unreachable under a
  transport (ResolveGlobalConstantsRecord, SyncCurrentProgram,
  BindCurrentProgramWithResources). The other two are kept BY CONTRACT (§5.5):
  GetBackendProgramId is a GL-thread entry point and ShaderStorageBlockBinding is reached
  from the barriered set_storage_block_binding, which is pg's trailing item.
2026-09-18 08:14:05 -04:00
swung0x48 112c1d8dfc [MG_Test] (Disaggregated): P5e vi - the two source decisions, the window rule and the client-array lane
- SanityTest's DirectGLESVertexInputDraw pair drives the PRODUCTION decisions
  (ResolveDrawVertexBuffersFromRecord / ResolveDrawIndexBufferFromRecord) with the frontend
  VAO deliberately pointing at a buffer the record does not name - the state a run-ahead
  client makes ordinary and one process cannot otherwise produce. Both functions take the
  applier state and nothing else, so the only revert is a frontend read inside them and
  the cases then go red naming the field (ID-102's rule about what a red-once may test).
  G2/G14 skip twins in the pull arm.
- VertexInputEmit pins ID-95 / ruling 19 on the producer side: set_vertex_buffers' window
  must cover every attribute create_vertex_elements declares ENABLED, with a sparse
  configuration so "covers the enabled set" is not "is as long as the array". The sink half
  is tx2's CheckUnitWindows.
- ClientVertexArrayScenario is BRIEF-P5E's open item (a), answered: NO existing scenario
  exercises a client-memory vertex array under split (DoublePrecisionScenario uses the
  binding-model calls against real buffers), so the lane had no entry that could reach the
  refusal at all. Registered in the split lane with a private log path, and it is also the
  positive control for keeping the upload arm alive under lockstep.
2026-09-18 08:10:26 -04:00
swung0x48 4fc93c1ab9 [MG_Remote] (Disaggregated): P5e vi - kDrawClientArrays on the wire, and the run-ahead refusal
- ReadDrawBindings answers ClientVertexArrays in the SAME single read of the bindings the
  rest of the draw plan is made from: an enabled attribute with no buffer behind it, which is
  exactly what EmitVertexBuffers publishes as Res == kMGPipeNullHandle.
- PlanDrawInfo sets kDrawClientArrays for every draw shape, not only the array ones - a
  glDrawElements can fetch its VERTICES from client memory too - because MGPipeBarriered is
  computed from the record on both sides and a null Res inside the window is also what a
  disabled attribute publishes.
- EmitDrawRecord and the reduced DrawArrays path refuse such a draw under run-ahead with
  Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"} on the GL thread, before the hooks
  (ID-82 / ruling 2). LOCKSTEP IS UNCHANGED: the client is parked in WaitForApplied for that
  record, so the server's upload reads memory that is not moving (ruling 4). Staging the
  bytes as a per-attribute {BindingIndex, MGHostSpan} tail is P8's.
- The armed predicate is spelled here from the three inputs MGPipeTypes.h names for
  RunAheadArmed() rather than called, because ClientSession::RunAheadArmed is package ra's;
  it is inert until the integration commit publishes the caps bit, and ra collapses this
  body into that latch.
2026-09-18 08:10:10 -04:00
swung0x48 a04cf05c93 [MG_Backend] (Disaggregated): P5e vi - the VAO, its buffers and its index buffer come from the records
- ResolveVaoTwin takes st.BoundVertexElements under a transport (id's by-handle body); the
  frontend overload survives as monolith glue and its MGPipeFrontendKeyedRegistryScope is
  DELETED, which is what makes a revert of the twin key abort instead of being exempted
  (CONTRACT-P5E §4.1, §4.4, §5.8). Two of this family's three scope sites go with it.
- SyncVaoAttributeBuffersByRecord beside SyncVaoAttributeBuffersByHandle: the walk is over
  st.VertexBuffers[Start..+Count).Res, deduped on {slot, gen}, ensured with
  EnsureBufferResourceForHandle(nullptr, Res) and probed with a null frontend. The
  push-monolith arm keeps its frontend walk token for token (ruling 1 / ID-81).
- The IBO arm reads st.IndexBuffer.Res with st.IndexBufferSerial, and the serial JOINS the
  identity compare because this arm has no live element slot to re-read: a re-emitted
  set_index_buffer on the same handle would otherwise read clean. ResolvedDrawBuffers gains
  iboSerial; Entry::frontend stays declared for the legacy arm and is never written here.
- PrepareForDraw stops reading GetBoundVertexArray and GetConfigVersion under a transport;
  SyncCurrentVertexAttributeValues keys on {elementsHandle, ContentSerial, activeMask} and
  reads rec->Attributes[i].Enabled. The two program reads there are pg's and stay.
- The DrawArrays / MultiDrawArrays client-array arms resolve the twin by handle and take the
  frontend VAO only when the RECORD says an enabled attribute is client-sourced, which is
  what retires GetBoundVertexArray@DrawArrays for every ordinary draw.
- The two source decisions are named functions over the applier state alone
  (ResolveDrawVertexBuffersFromRecord / ResolveDrawIndexBufferFromRecord) so a unit case can
  drive the production decision rather than a copy of it.
- BumpBufferMutationEpoch pins BRIEF-P5E's open question 2: a bump off the apply thread while
  one is running is counted and Fatal under the strict lane. The measured answer is that the
  only non-apply-thread bump in the tree is BackendObject_DirectGLES::Initialize's
  RegisterBufferBackendOps, before the apply thread exists.
- The restart helpers refuse a frontend element-slot read under a transport by name; the
  substitution's own body already resolved from the applier since P5c.
2026-09-18 08:09:53 -04:00
swung0x48 fcd7c5c87a [MG_Backend] (Disaggregated): P5e fb - the eager image-bind funnel stops reading the frontend binding
- The strict lane named GetImageTextureBinding@BindImageTexture on thirteen entries:
  bind_shader_image is an unbarriered row (CONTRACT-P5E §2.2) whose backend entry point
  receives GL arguments and a GL NAME - MGPImageBind::Res stops at the sink - so this
  funnel had no way to resolve the twin and read the frontend binding instead.
- Under a transport it now records only the high-water mark and returns. Nothing is
  lost: the sweep at the next validate point re-binds every unit up to the mark from
  the applier's own MGPImageView array, and the sink bumps the texture shutter serial
  immediately after this call, which is one of the four values the sweep's gate is
  keyed on - so the sweep that follows a bind cannot be suppressed. An image unit has
  no reader but a shader, and no shader runs in between.
- The mark itself must still be raised here, because it is the "no draw in this context
  can be reading an image" early-out.
2026-09-18 08:09:43 -04:00
swung0x48 1ebe6abeb2 [MG_Pipe, MG_Impl, MG_State, MG_Remote, MG_Backend, MG_Test] (Disaggregated): P5e sb - set_shader_buffers is emitted, routed and applied
- MGPShaderBuffers::WritableMask is Uint32[3] and the POD grows 32 -> 40 (ID-104). One
  Uint32 could describe only the first 32 of an 84-point window, so a storage buffer bound
  at point 32 or above read as unwritable with nothing able to see it; the ruling widens the
  field rather than narrowing the window, which would change what an application may bind.
  MGPipeShaderBufferMaskHas / ...Set are the one spelling of the mask, so the emitter, the
  applier and the four backend sites cannot disagree about which word a point lives in.
- MGPipeApplySetShaderBuffers has its body: per-class window, Start/Count validated against
  the capacity, the mask copied by word, ONE ShaderBuffersSerial advanced on every applied
  record and ADVANCED - never zeroed - by both resets, which also clear the three windows.
- Route rows for opcode 38 on both arms (Mono_ and Wire_) and the server sink in the wire
  codec's decode switch, which is where a kCtxState row's sink lives - PipeApplier.cpp's
  ServerVerbSink has no method for it and the brief's "OnSetShaderBuffers" names no code.
  The codec also bounds Class and Start+Count where Count becomes a byte length.
- New MG_Impl/Pipe/ShaderBufferEmit.h: one record per class, window = the touched
  high-water mark clamped to 84, zero early-out before any hash, a BASE binding travelling
  as kMGPipeWholeBuffer so the server re-resolves the extent at use, three suppressor slots
  (ruling 11), and the writable mask that replaces the backend's GPU-write walk.
- BufferState gains a per-target bind-point generation (push-only, G1) and dirty bits
  15/16/17 shutter on it instead of the buffer CONTENT aggregate. That aggregate answered
  the wrong question in both directions: any glBufferSubData fired all three, and
  glBindBufferBase fired none of them, because it mutates a binding point through a returned
  reference. Same defect class as P4a's glBindSampler hole.
- Init.cpp publishes consumer bit 13 for DirectGLES and not for Magma (ID-106), in the same
  commit as the wired constant, and PipeFill's liveness gate asks R-8 for bit 13 in
  particular plus bit 13-requires-bit-7.
- `Touch` joins the dirty-surface scanner's mutator prefixes; TouchBufferBindingPoint gets
  its row and SetNamedTransformFeedbackBinding's is rewritten now that bit 17 reads a
  generation a binding moves. Negative control 6 is re-pointed: the pair it used to refute
  is a live answer, and a control that has quietly become positive is what the self-test is
  for.
2026-09-18 08:08:04 -04:00
swung0x48 f4675d1ac0 [MG_Backend] (Disaggregated): P5e fb - SCAFFOLD, REVERTED BEFORE HEAD: tx2's two by-handle seams, so fb can measure its own lane
- fb calls TextureImpl::SyncTextureToBackendByHandle and
  BackendTextureObject::SyncMipmapsToBackendByHandle from four places. tx2 owns those
  bodies and is being written in parallel, so with c0e's aborting stubs every
  integration-split entry that touches a texture dies on the seam and fb's own isplit
  and strict numbers cannot be read at all.
- This commit bodies the two temporarily, in the one way that does not pre-empt tx2's
  design: the frontend texture is resolved from the record's own GL name through the
  BARRIER-PULLED sticky forward GetTextureObject, exactly as the copy-image sink
  resolves its endpoints, and the existing frontend overloads do the work. Correct
  under lockstep, and deliberately NOT the shape tx2 will write - tx2 drives the
  storage from the descriptor and holds no frontend object.
- IT IS REVERTED BY THE NEXT-BUT-ONE COMMIT. It exists in history so the integrator can
  see exactly what fb assumed about the seam and what fb's measured numbers rest on.
2026-09-18 08:06:59 -04:00
swung0x48 a08b337975 [MG_Test] (Disaggregated): P5e tx2 - the seven shape reads become pinned guard rows, and ABA gets its own case
- RemoteClientTest's MGL_TEXTURE_GUARD_TEST block gains the seven MipmapStorage shape
  reads P5c deliberately exempted, with the sentence that exempted them ("the per-draw
  binding walk reads them every draw") replaced by the reason it is no longer true.
- SanityTest gains ARecycledTextureSlotReMintsTheTwinOnTheHandleArm beside
  UnitMemoRefusesToDriveATwinFromAnotherTexture: the borrowed-slot list needs a pointer
  compare because its key is derived state; the by-handle list needs none, because a
  recycled slot arrives as {s, g+1} and the forward-Gen reset gives the successor a fresh
  twin while the predecessor's handle stops resolving. G2/G14 skip twin included.
2026-09-18 08:06:36 -04:00
swung0x48 6c37c20f59 [MG_Backend] (Disaggregated): P5e fb - the FBO doors, both attachment lists, the binds, the blit and the detach walk read records
- The doors: RefuseFramebufferBindingSlotRead raises
  Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"} where a decline used
  to fall through to the pre-handle arm. Under a transport with the framebuffer bit
  set the fallback is not a safe default - it would read a row belonging to a client
  this apply is no longer synchronous with - so SyncCurrentFBO's and BindCurrentFBO's
  declines become the detector CONTRACT-P5E §5.4 asks for.
- The two attachment texture lists are the package's core: the one unconditional
  per-draw live read of this family, taken whether the memo hit or missed. They are
  now keyed {Fbo, ContentHash, ContextSerial, g_backendContextGeneration} with entries
  {Res, twin} that borrow nothing, and the dedupe asks the two BOUND HANDLES rather
  than two pointers. Three frontend reads go in one move: both binding slots, the
  FramebufferObject the entries borrowed their attachment slots from, and
  GetTextureContextId.
- SyncCurrentFBOByRecord drives the twin through SyncToBackendByHandle /
  SyncReadBufferToBackendByHandle; NoteStateForHandle is gone, which was the server's
  last cross-record hold on a frontend framebuffer.
- FramebufferRecordMatchesBinding is monolith-only: the record was resolved from the
  applier's own bound handle, so there is no second answer left to corroborate.
- ForceBindCurrentFBO reads the binding slot INSIDE the else instead of on both sides
  of its own handle arm (S3 finding #7); the three legacy stamps go with it, which is
  safe for the reason the serial memo is invalidated rather than stamped there.
- SyncAndBindFramebufferByHandle configures from the record, so a DSA framebuffer that
  was never bound is configured correctly instead of being named and bound unconfigured.
- Both blit arms take two const MGPFramebufferState&: draw buffers from DrawBuffers[],
  the read buffer by matching ReadSurface against Color[], kind/level/layer/layered/
  format from the surface, Samples from the record, the textures by handle.
- The detach walk uses the reverse index and the records instead of ForEachLive +
  StateForHandle + the frontend attachment array. The frontend-keyed scope stays at
  this site: every caller reaches it from a barriered row (§4.4).
- SyncRenderbufferObjectToBackend refuses by name under a transport - its only caller
  is the copy-image endpoint, whose renderbuffer form both the emitter and the sink
  already refuse - and the scope at that site is deleted.
- Clear's debug-only draw-FBO diagnostic reads the record under a transport.
2026-09-18 08:02:24 -04:00
swung0x48 859590901a [MG_Backend, MG_State] (Disaggregated): P5e tx2 - the two the lane found: Magma's blit read and the copy endpoint's own sync
- The widened MipmapStorage rows are backend-kind-keyed (ruling 12's shape): Magma is
  lockstep through P7 and its named blit still asks a frontend texture IsComplete()
  (VulkanRenderer's ResolveColorBlitBinding), which the unconditional widening aborted.
  The MUTATOR rows stay unconditional - no backend may write a frontend texture's storage
  from the apply thread, and none does.
- CopyTexImage2D / CopyTexSubImage2D sync their DESTINATION by handle instead of merely
  looking its twin up. A copy destination is not sampled by the draw program, so the
  sampler-view window never brings it across; the pre-P5e frontend walk over every slot of
  every touched unit happened to sync it as a side effect. This is the "waited texture
  ops" term of the named narrowing (P5e-5) being made real rather than assumed.
2026-09-18 08:01:22 -04:00
swung0x48 6e6b86e592 [MG_Remote, MG_Pipe, MG_Impl, MG_Backend] (Disaggregated): P5e ra - the wait rule, the present credit and server-owned gPipeInputs
- CONTRACT-P5E §2.3. EmitAndWaitTails now switches on the generated WaitClass column
  when RunAheadArmed(): kWaitNone and kWaitPresent return at the publish, kWaitApplied
  waits and then releases the fill pins, and a reply row is unchanged. It REPLACES the
  BatchWaits skip rather than layering on it - the batch rule reads the call CLASS and
  this one reads what an apply READS, and the server's MGPipeBarriered reads the second,
  so two rules layered is two halves of the wire disagreeing about whether the client is
  parked.
- §1: RunAheadArmed() = barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply),
  latched at the first caps adoption after Start and demotable but never promotable - a
  record already published without a wait cannot be un-published. A knob set against a
  server that does not publish the bit logs once and runs lockstep.
- §2.4: MGPPresent::FrameSerial is minted client-side, 1-based, by AcquirePresentCredit,
  which pays the credit BEFORE the encode (a client that encoded first would hold a
  SEG_CMD reservation across the park). ServerVerbSink::OnPresent returns one credit per
  swap through ServerSession::ReturnPresentCredit, which has had no production caller
  since v1 wrote it; serial 0 on a run-ahead server is Fatal{ProtocolCorruption,
  "Present.FrameSerial"}. The stats line and the wire ledger gain credit-waits.
- §2.5, the drains and the forced waits: every wait exit drains SEG_EVENT (the applied
  wait, the present-ack wait, the cmd-space wait and - through a hook the session lends
  the encoder - the stage bell); glFinish becomes WaitForApplyToCatchUp, since "the
  commands issued so far have completed" is a promise a queue nobody waits on can break;
  every Server* EGL forwarder waits first, because the control mailbox is pumped BETWEEN
  drain batches and a make-current landing between two run-ahead records is not a wrong
  pixel but a wrong everything. ServerSwapEGLBuffers is not on that list: present is the
  swap.
- §2.5 / ruling 15 (ID-93): resource_subdata's BUFFER half stops waiting. The row keeps
  kReplySlot, the server still posts, and the slot is never read - which is what makes
  the 64 KB persistent-map push fire-and-forget instead of a hidden round trip per block.
  MGPipeSubDataWantsItsReply is the one predicate, read by the route and by the emit
  table, so the two cannot disagree about whether a wait is owed. The texture half keeps
  its reply: DrainTextureSubData clears a level's dirty flag on the accepted answer.
- §3: gPipeInputs becomes server-role memory for unbarriered records. The tracker walk
  and the emitters still run - they produce records - but the serial bump, the verb
  stamp, the server-stamp withdrawal and the whole of step 4 do not, and
  MGPipeNoteFrontendMutation / MGPipeLeaveVerb return instead of writing. `fillOwed` is
  a separate name from `barriered` on purpose: the red-once is that one line.
- §3.3: MGPipeInputUnfreshRead's counter becomes an UNCONDITIONAL abort under an
  unbarriered record, knob or no knob, because the value is torn or stale by
  construction and there is nothing for a counter to count. The marker gains its reason
  so the lane's allowlist can tell the two apart.
- §3.5: RefusePipeInputsTouchWhileApplierOwnsIt is re-derived - under run-ahead the rule
  is "not at all unless you are the barriered fill", and the BatchWaits early return
  goes with the argument that justified it.
- §2.3's ReleaseFillPins drops the four O-class SharedPtr rows once a barriered apply
  returns: under lockstep the next fill overwrote them within a verb, under run-ahead a
  pinned VAO can make the apply thread its last owner a frame later.
- §2.7 / ruling 13 (ID-91): the deferred-destroy queue stays live, and an enqueue from
  an UNBARRIERED apply is now a named finding - loud once, Fatal under strict.
- NAMED DEVIATION from §3.1: the identity/liveness pair is still written on every verb.
  It is not a field of the residual model - no ownership row owns it, no record carries
  it, no server stamp can answer it - it is "does this process still have a GL context",
  which only this thread knows and every null-context guard reads.
- Inert until the integration commit: RunAheadArmed() is false while
  kMGPipeP5eRunAheadReady is, so every branch above answers as it did before.
2026-09-18 07:58:06 -04:00
swung0x48 a3466e6f4c [MG_Remote] (Disaggregated): P5e ra - SEG_EVENT stops aborting and starts flow-controlling
- CONTRACT-P5E §2.6. Fatal{EventRingOverflow}'s premise was "the client drains at
  every verb, so a full 256 KiB ring is a burst no workload has". A run-ahead client
  drains at its waits, which on the steady path are one present apart, so a full ring
  is an ordinary backlog and aborting on it would turn the reverse channel's capacity
  into a workload limit.
- The producer blocks instead (ReserveEventOrBlock, one entry point for all four
  event kinds): publish what is in the ring, ring the client, park on this server's
  own bell until the latch clears, retry. A server that does not publish
  kCapRunAheadApply keeps P5C's Fatal byte for byte.
- Two named refusals survive, and they are the impossible cases rather than the busy
  ones: a single event larger than half the ring (no drain can ever make room) and a
  30 s wait for a client that is not draining because it is not running.
- The deadlock argument needs both halves, so both are here.
  EventRingConsumer::Drained() now CLEARS AND RINGS - a cleared flag with no bell is
  the lost wakeup the forward direction's publish-then-ring order exists to prevent -
  and SessionProducer gains WaitForAppliedOrEventBacklog /
  WaitForPresentAckOrEventBacklog so a parked client is always wakeable by the server
  that is waiting for it. The old two waits keep their callers: teardown has no ring
  to drain and must not learn about one.
- ServerLoop's park condition gains a fourth clause, and it is the one that says "not
  now" rather than "wake up": with the ring full a waiting record is not work yet,
  because applying it would only park the producer half way through. Stop and Control
  still win, so a teardown is never held up by a client that stopped draining.
- Inert this phase: nothing publishes kCapRunAheadApply until the integration commit,
  so every arm above answers as it did before.
2026-09-18 07:57:33 -04:00
swung0x48 72e7902080 [MG_Backend, MG_State, MG_Remote] (Disaggregated): P5e tx2 - the texture family by handle, the unit window, and the guard that notices
- SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle get their bodies
  (the c0e seams): record first, twin by GetOrCreateByHandle, three syncs from the
  record, no frontend touch and no allocator probe on the path.
- BackendTextureObject carries m_pushedSyncHandle (CONTRACT-P5E §4.1's m_handle), which
  is what the three sync prologues read instead of probing the client allocator with
  HandleOf, and what selects the by-handle arm inside them.
- IsDrawSyncCleanByRecord replaces IsDrawSyncClean(frontend) on the memo-hit path: the
  same conjunction over the record's serials and the twin's own, with BOTH halves of each
  resync pair read and neither clearing the other's (D-E2, scout R4).
- The unit work list is built from st.BoundSamplerViews[] and keyed on
  (ContextSerial, SamplerViewsSerial, window, backend context generation); PairingsIntact
  is not carried on it, because a {slot, gen} has no stale-pairing hazard to catch.
- ResolveAndBindUnitTextures is one pass over the window with the target from the view
  CSO, then an unbind driven by g_boundTexturesCache; the alias arbiter dies (the client
  arbitrated, SamplerEmit.h).
- BindCurrentTextures' memo key is ruling 6's both halves, entry selection included.
- The sampler pass reads BoundSamplerStates / the built-in sampler CSO / the view's
  InternalFormat; the apply-thread mint and ResolveUnitSamplerBackend become a named
  refusal under a transport (§4.2).
- GenerateMipmap resolves VerbMipRes and never the active unit; the detach sweep is keyed
  by the texture handle over the applier's framebuffer records. The row is NOT flipped to
  kWaitNone: the RGB16F/RGB32F CPU filter still reaches the client's level shadows, so
  ruling 14's escape applies and the flip is trailing.
- RequireImageBindableStorage gets a by-handle entry whose re-dirty is
  Fatal{UnmigratedEmulation, "image-bindable-redirty"} at the entry rather than three
  frames deep at a MarkStorageDirty guard.
- The buffer-texture arm resolves scout R5 with Desc.BufferForTexBuffer / BufOffset /
  BufSize and EnsureBufferResourceForHandle(nullptr, h) - that function is client-free
  under an active transport (kimi audit row 29).
- CheckUnitWindows(st) at draw and dispatch makes the §5.3 window promise checkable and
  checked: narrower than [0, MaxTouchedTextureUnit] is
  Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count".
- MipmapStorage's guard list gains the read-only shape accessors P5c deliberately
  exempted, now that nothing on the draw path reads them.
2026-09-18 07:56:34 -04:00
swung0x48 3012d52e2b [MG_Backend] (Disaggregated): P5e fb - the attachment surface, the two by-handle twins and the image unit read records
- SyncAttachmentSurface loses its frontend attachment parameter (CONTRACT-P5E §5.4).
  The storage syncs have by-handle forms now, so the object was only there to be
  cross-checked - and a second answer only a monolith build can produce is not a
  check. Review N-6's one-directional hole ("record empty, frontend still holding")
  becomes unrepresentable: the caller's detach and the attach read one test.
- The renderbuffer cross-check at the old Managers.cpp:9711 is DELETED rather than
  transport-gated (§4.4). It was the one HandleOf in this family that was not gated
  while its texture sibling was; it ran a client-allocator probe on the apply thread
  under split, legal only because the scope named it and the client was parked.
- BackendFramebufferObject::SyncToBackendByHandle and
  BackendRenderbufferObject::SyncToBackendByHandle land in place of c0e's aborting
  seams, beside the object forms they overload. The record's eleven surfaces are the
  point set; the per-point frontend version memo is gone and the record hash is the
  only gate, with g_attachmentBackendIdGeneration and the re-entry kept because no
  client-side value can answer what they answer.
- SyncReadBufferToBackend's decision moves into ApplyReadBufferFromRecord, which takes
  no frontend object; both overloads funnel through it.
- The reverse index texture -> framebuffers, maintained at the by-handle sync, for the
  detach walk (tx2 needs it) instead of a walk over every twin's frontend attachments.
- Images: ResolveShaderImageRecord and its identity probe are gone - under a transport
  there is no second answer to arbitrate. SyncImageTextureBinding gains a record
  overload; Access is DECODED through MGPipeValueTypes.h (ruling 16 / ID-94) and the
  stale "the encoding does not exist" comment is deleted. The bind's format rules are
  factored into one helper both arms call rather than written twice.
- The image sweep gate is re-keyed onto (ShaderImagesSerial, TextureShutterSerial,
  ContextSerial, g_backendContextGeneration) under a transport (ruling 7 / ID-86),
  never a backend re-mint counter; the window/high-water union is kept and the walk
  reads BoundShaderImages[].
- MarkWritableImageBufferTexturesGpuWritten returns early under a transport - the
  client owns the GPU-write set; paired with sb's deletion of the backend marks.
- BoundImageUnitFormat answers from MGPImageView::InternalFormat under a transport.
2026-09-18 07:53:05 -04:00
swung0x48 39d0677616 [MG_Pipe, MG_Impl, MG_Remote, MG_State, MG_Test] (Disaggregated): P5e pg - the archive becomes server-owned and the bindings get a record
- MGPipeShaderCsoRecord gains Archive, a SharedPtr<const ProgramArchive> the create
  ADOPTS under a transport. Before this the applier stored the descriptor and the two
  companion pointers named the frontend's own LinkArtifacts/SpirvArtifacts, which
  ProgramObject::Link() replaces in place - so every reflection read on the apply thread
  was torn or stale by construction the moment the client runs ahead (gap G-A).
- MGPipeApplyCreateShaderState takes the archive as a fourth argument, undefaulted for
  PipeRoute.h's stated reason: a caller that has one and forgets it would compile, and
  the failure would be silent. Null on the monolith arm, which keeps its frontend arms
  token for token (ruling 1 / ID-81).
- ProgramArtifactsCodec gains ProgramArchive and the FRAME. SpirvArtifacts carries no
  stage list and MGPProgramDesc::StageMask cannot stand in for one: it is a bit set, and
  GL lets two shader objects share a stage, so a list rebuilt from it can be shorter than
  generatedSpirv - which the backend pairs with it by one running index.
- Codec completeness, as CONTRACT-P5E §5.5 asks: the skipped member is named
  (LinkArtifacts::program, the live glslang TProgram) and proven unread by either backend
  twin; the libc++ caveat at the size echo is read as what it is - a cross-toolchain
  check, exact for an in-process split - and both verdicts are written where they were
  asked for.
- set_program_bindings (opcode 80) lands end to end: EmitProgramBindings on the client,
  the FIFTH ROUTE ESCAPE (three tails in three index spaces plus a parallel name array -
  no generated row can express it, so op 80's gMGPipeContext slot stays null as
  PipeCatalogueTest pins), the wire emitter, the decoder arm that resolves and
  NUL-checks every name span, and the applier body that copies the names into the record
  (rule C) and advances BindingsSerial.
- The record is emitted AFTER create_shader_state, not before as the draft note said: a
  re-issued create clears all three tails because they index into the archive it just
  replaced, so bindings published ahead of it are wiped by the create that follows.
- MGPProgramDesc::LinkStatus is filled, and ID-88 is VERIFIED rather than assumed:
  ProgramObject::Link()'s prologue bumps m_linkVersion and assigns m_artifacts = {} -
  "the complete not-linked state" - before the body runs, and every ProgramLinkTask
  failure arm leaves linkStatus false. A failed relink therefore reports unlinked, the
  re-issue carries LinkStatus = 0, and nothing needs an object_death.
- ID-87's measurement gets its instrument: the archive's byte count is added to
  PipeStats' CsoBlobBytes where the bytes actually exist, one sample per link.
2026-09-18 07:50:27 -04:00
swung0x48 f6cfcbd36b [MG_Pipe, MG_Backend, MG_Remote, MG_Test] (Disaggregated): P5e wave 1 integration - one body per seam, and the stamp waits for ra
- c0e and id were implemented in parallel and each declared the two shared seams,
  so the merge produced five duplicate definitions the text could not collide:
  MGPipeBarriered, MGPipeApplierCurrentRecordIsBarriered and the three by-handle
  twin resolvers. Resolved per ID-99 and ID-103.
- MGPipeBarriered keeps c0e's real body (the static wait-class column, the XFB
  escalation and the client-array escalation); id's constant-true placeholder goes.
- The current-record flag keeps c0e's thread_local storage and setter and loses
  id's MGPipeApplierState member: it describes which record THIS THREAD is inside,
  not per-context state the applier owns, and the allocator guard that reads it
  would otherwise have to find the right context's applier first. id's test hook
  posts through the setter.
- The three resolvers keep id's real bodies beside each registry; c0e's asserting
  stubs for them go. The seam stubs no package has bodied yet stay.
- ApplyOne stamps `true` while kMGPipeP5eClientWaitRuleLanded is false. The
  predicate says what the client WILL do; until ra changes EmitAndWaitTails the
  client still blocks after every record, so a `false` stamp would withdraw the
  §4.4 exemption from a probe the client's own wait still makes safe. The
  predicate is computed on every record regardless, so it is exercised for the
  whole phase instead of first running on the day it starts deciding.
- Gates on the merged tree: unit 2206/2206, integration-split 111/111, both
  generators --check/--self-test green, include closure 0 problems, dirty surface
  rc 0, doc citations one pre-existing CONTRACT-P5.md line (c0e retired another).
2026-09-18 07:05:48 -04:00
swung0x48 367e473416 Merge branch 'p5e/id' into feat/disaggregated (P5e id: handle-keyed twins, the barriered exemption, the scope rename) 2026-09-18 07:00:07 -04:00
swung0x48 11cbf4b688 Merge branch 'p5e/c0e' into feat/disaggregated (P5e c0e: the contract, the wait-class column, opcode 80, the knobs and the cross-package seams) 2026-09-18 07:00:07 -04:00
swung0x48 a513c53ee3 [Docs] (Disaggregated): P5e c0e - fix the one citation the draft got wrong
- SlotAllocator.cpp:410-419 named lines that do not exist; the Gen-on-reuse rule is at :176-189
  for the ordinary table and :240-258 for the composite band. The doc-citation lint now reports
  the same four pre-existing problems it reports at the base and no new one.
2026-09-18 06:50:23 -04:00
swung0x48 b4cf9b0527 [Docs, MG_Remote] (Disaggregated): P5e c0e - the contract lands, and the roadmap row
- MG_Remote/CONTRACT-P5E.md: the draft edited into its landed form. The nineteen rulings are
  as the integrator decided them (ID-80..98) and are restated here rather than referenced,
  because a package reads this file and not the brief. The "draft" wording is gone and every
  file:line was re-resolved at this head.
- FOUR PLACES WHERE THE PLAN WAS WRONG AND THE CODE IS RIGHT, each argued where it sits: the
  descriptor has no spare byte so MGPProgramDesc grew 192 -> 200; the applier's per-class
  windows are ShaderBufferStart/Count/WritableMask, not bare Start/Count/WritableMask; the
  three suppressor slots are named per Class; and the wire codec's layout carries three tails
  now, because set_program_bindings has three and the third one's offset is derived from the
  first two.
- One finding the contract now carries rather than leaves for sb to discover: MGPShaderBuffers'
  WritableMask is a Uint32 and the window is 84, so the mask can only describe the first 32
  points of a class. sb asks for the ruling; it is a wire field and not a package's to pick.
- A new section 10 lists, by name, every symbol c0e put in the tree and what each package
  compiles against - plus the three red-once evidence lines.
- ROADMAP.md gains the P5e row after P5d (status, the eight packages in dependency order, the
  integration gate and the device exit) and the two lines that said P6 was next now say P5e is.
  CURRENT_STAGE_PROGRESS.md section 1 gains its row and section 6 its next-step entry.
2026-09-18 06:49:46 -04:00
swung0x48 c06878ef73 [MG_Pipe, MG_Impl, MG_Backend, MG_Test] (Disaggregated): P5e c0e - the cross-package seams
- MGPipeBarriered(op, payload, applierState), IMPLEMENTED, not stubbed: the static wait-class
  column plus the two payload-derived escalations the contract names (an open XFB span, a draw
  carrying kDrawClientArrays). One function and two callers is the point - the emit table and
  the sink must reach the same answer or the client runs on while the server reads a value that
  has already moved. set_context_values now mirrors IsTransformFeedbackActive into the applier
  state as well, because a predicate that read it out of gPipeInputs would be reading the very
  block run-ahead stops filling.
- MGPipeApplierCurrentRecordIsBarriered() with the writer beside it. It answers true by default
  and that IS the landed lockstep answer, not a stub: on a server without kCapRunAheadApply
  every record is barriered and CONTRACT-P5C's two exemption scopes keep working unchanged.
- The sb applier fields (BoundShaderBuffers[3][84], the three windows, the writable masks and
  ShaderBuffersSerial) and the pg record fields (the three binding tails, Signature,
  BindingsSerial) with the two entry points MGPipeApplySetShaderBuffers /
  MGPipeApplySetProgramBindings declared and ABORTING BY NAME.
- The by-handle backend seams in Managers.h with aborting bodies in one block: ResolveVaoTwin,
  ResolveTextureTwin, ResolveProgramTwin, SyncTextureToBackendByHandle,
  SyncMipmapsToBackendByHandle, and the FBO / RBO / program SyncToBackendByHandle overloads.
  Five packages sit on opposite sides of these calls - tx2 writes what fb calls, fb writes what
  tx2 needs, id rekeys the registries all three read - so the signature is fixed once here and
  each family replaces a body. Abort and not null: a resolver that answered null renders a
  blank draw and a sync that did nothing renders stale pixels, and both are green lanes.
- Coverage.def's GetBufferBindingPoint -> SetShaderBuffers row with BOTH of its PipeFill.cpp
  switch arms and the pairing asserts, shape-only and inert. It lands with the contract package
  because PipeFill.cpp belongs to the contract package for the whole phase - an emitter
  enumerator whose dispatch arm lived in the family's worktree is the merge trap that file's
  own comment was written about.
- FieldOwnership.def re-annotates the six object-class rows plus GetProgramForDispatch to
  "P5e (Espryt unbarriered), P7 (Magma)", and the strict-abort case follows the string, which
  IS the debt entry rather than decoration. kMGPipeMaxBufferBindingPoints is pinned against
  BufferState.h's 84 where both are visible. gen_pipe_field_ownership's refusal count is 8.
2026-09-18 06:40:43 -04:00
swung0x48 0117d15725 [MG_Test] (Disaggregated): P5e id - the band case, and the exemption scope stops exempting an unbarriered apply
- New DirectGLESSlotTable.ACompositeHandleDoesNotGrowTheOrdinaryTable (with its
  G2/G14 skip twin) pins the band: a composite handle grows m_band by four
  entries and the ORDINARY table by none, the lookup finds it, an ordinary slot
  beside it disturbs neither space, ForEachLive reports the composite at the slot
  THE CLIENT MINTED rather than at its band index, and §4.3's recycle answer
  holds inside the band. It asserts on the two spaces' CAPACITIES, not on
  liveness: the table holds one live entry either way, so LiveCount could not see
  the growth this case exists to catch.
- RemoteGuards.AnAllocatorProbeInsideAnExemptionScopeOnTheApplyThreadIsAllowed is
  DELETED. It asserted that a named scope exempts unconditionally, which is the
  rule CONTRACT-P5E §4.4 retires. Its replacement,
  ...FromTheApplyThreadIsFatalEvenAtTheOldDebtSites, opens the same scope with
  the applier's CurrentRecordBarriered forced false and expects HandleOf to
  abort. The hook is the production field ApplyOne stamps, not a test-only branch
  beside it; the texture and the table are built on the CLIENT thread, because a
  TextureObject2D's constructor mints and building it on the apply thread would
  abort at the mint with the same diagnostic - a case passing for the wrong
  reason.
- The green half of the deleted case needs no replacement: every Espryt
  integration-split entry runs the frontend-keyed scope's surviving sites on the
  apply thread behind barriered records, so a guard that stopped exempting would
  take the whole lane down.
- Unit 2203 -> 2204 (the new name only adds); integration-split unchanged at 111.
2026-09-18 06:36:07 -04:00
swung0x48 287bade924 [MG_Backend] (Disaggregated): P5e id - the NoSession mailbox hop deleted, the twin death moment and the ABA answer written down
- OnFrontendStateObjectDestroyed's split arm loses the mailbox hop
  (CONTRACT-P5E §4.4). It existed for the ONE shape the object_death record
  cannot carry - a transport with no client session at all, i.e. a ServerLoop
  fixture driving a server role with no client, never a real split - and it
  posted a stack struct to the apply thread, which re-entered this function and
  ran DestroyByLifetimeId: a probe AND a Free in the CLIENT'S allocator from the
  apply thread. That is not a read a barrier could ever make safe, because under
  run-ahead there is no wait behind which the allocator stands still. Such a
  fixture's twins die with the backend at Stop, which is what already happened
  for every death raised past Running() == false.
- The frontend-keyed scope this function opened goes with it: the one arm that
  needed it was the hop. Falling through under a transport now means the death
  was raised ON the apply thread - the server backend destroying a frontend
  object it created itself - and those sites carry their own named scope.
- ID-96, ANSWERED BY MEASUREMENT rather than by reading: a probe that aborted in
  exactly the deleted branch (NoSession with a RUNNING server loop) was run over
  the whole unit lane (2204 entries) and the whole integration-split lane (111)
  and NEVER FIRED. The only fixture family that could reach it is
  ServerLoopTest.cpp's ServerFixture / EglServerFixture - the tree's only server
  role with no client session - and its one case that lets a re-keyed frontend
  object die off the apply thread
  (ServerLoopEglTest.FrontendFramebufferDeathDeletesOnTheContextOwner) is already
  refused at its FIRST step by P5c's allocator guard. So the set of fixtures that
  lose death delivery is empty: none rewritten, none deleted.
- ReleaseTwinsForWireObjectDeath gains §4.2's death moment and §4.3's ABA answer
  in its own voice: the ordering is the argument (every record naming {s,g} is
  applied before object_death{s,g}, which is applied before create{s,g+1}) and
  the Gen compare is the proof (forward re-mints, a stale handle resolves to
  null, a backward generation is refused) - so a skipped death is answered by the
  forward-Gen reset alone and no wait is added at any delete.
2026-09-18 06:35:49 -04:00
swung0x48 0d54de62e2 [MG_Backend] (Disaggregated): P5e id - ResolveVaoTwin / ResolveTextureTwin / ResolveProgramTwin by handle
- The three by-handle resolvers the per-family packages call, in the shape
  SamplerImpl::ResolveSamplerCsoTwin already runs (CONTRACT-P5E §4.1/§4.2):
  handle in, THE RECORD FIRST, then AdoptTwinByHandle, then a twin if the slot is
  empty. A handle with no applier record is a seam defect and adopting a slot for
  it would leave a twin nothing can sync, so it is a loud null rather than an
  empty twin; a generation behind the live entry's is refused by
  PipeTwinHandleIsAdoptable rather than dropping the incumbent's driver object.
- WHAT THEY DO NOT DO is the deliverable: no frontend object is touched and the
  client's slot allocator is never probed. They also do not SYNC - which serials
  gate a VAO / texture / program sync, and what the sync reads, belongs to vi,
  tx2 and pg, and a resolver that synced would have to know.
- ResolveProgramTwin resolves through the band-aware PipeShaderCsoRecordForHandle
  and adopts into SlotTables.h's m_band, which is why the band landed in the same
  package: after the rekey a program-pipeline composite's bind is the ordinary
  path through this function.
- Declared beside each family's registry global rather than in the registry class
  region BRIEF-P5E names, so that the three read exactly like the sampler
  resolver they copy. Reported to the integrator as a range widening.
2026-09-18 06:35:24 -04:00
swung0x48 69a278549b [MG_Backend] (Disaggregated): P5e id - the composite band, ForEachLive by handle, the frontend-keyed half as named monolith glue
- BackendSlotTable gains m_band, the ShaderCso composite space indexed by
  (slot - kMGPipeShaderCsoCompositeSlotBase), beside the vector the ordinary
  slots live in - the shape MGPipeSlotAllocator::KindState and the applier's
  CompositeShaderCsos already use. EntryAt used to index m_slots by slot and
  resize to it, so ONE program pipeline grew g_backendProgramObjects to ~983k
  entries (~40 MB). That was reachable only through GetOrCreate(StatePtr) for a
  composite program; after id's rekey the by-handle resolution IS the ordinary
  path, which is why the band is a P5e prerequisite and not a follow-up.
  EntryOrNull / EntryAt / IndexOfSlot are the one place either space is chosen.
- ForEachLive is re-typed fn(MGPipeHandle, const BackendPtr&) (§4.1): a walk that
  handed a frontend SharedPtr out of server memory on every step was the last
  place the server could hold one across records. Its single caller,
  ScopedDetachedTextureFramebufferAttachments, asks StateForHandle for the object
  instead - one named read inside the scope that owns the debt. The set of
  framebuffers walked is unchanged: an entry the old walk skipped because its
  stateRef would not lock is one StateForHandle answers null for.
  StateBackendObjectRegistry::ForEachLive is now handle-arm-only; the legacy map
  has no handle to hand over and its one caller has always had its own
  begin()/end() walk beside the call.
- The frontend-keyed half is KEPT, not deleted (§5.8, ruling 1): the push build
  under Transport=monolith keeps its frontend arms token for token, which the
  PIPE_VERIFY comparator needs and which is what makes RUN_AHEAD=0 a pure
  wait-rule A/B on identical server code. Every one of the six surfaces is a
  named Fatal from an unbarriered apply - the allocator guard for the three that
  call the allocator, MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply for
  NoteStateForHandle / StateForHandle. No silent path.
- NoteStateForHandle / StateForHandle move from MOBILEGL_BUILD_DISAGGREGATED to
  MOBILEGL_PIPE_PUSH, because the re-typed walk's caller needs them in the
  push-monolith build too.
2026-09-18 06:35:08 -04:00
swung0x48 f40a57cc96 [MG_Pipe, MG_Impl, MG_Remote, MG_Backend] (Disaggregated): P5e id - the barriered seam, the allocator guard and the two scopes
- CONTRACT-P5E §2.1's predicate gets its two names in MG_Pipe/PipeApply.h:
  MGPipeBarriered(op, payload, st) and MGPipeApplierCurrentRecordIsBarriered(),
  with MGPipeApplierState::CurrentRecordBarriered as the per-record flag that
  PipeApplier::ApplyOne stamps before every decode. The body answers TRUE for
  every record until ra lands the wait rule, so lockstep is unchanged and this
  package alters no behaviour; ra rebases onto that line rather than adding it.
  Package c0e owns the final declaration of both names - the overlap is one hunk.
- §4.4: MGPipeRefuseAllocatorFromApplyThread now exempts a probe only inside a
  named scope AND while the record being applied is barriered. The client's wait
  is the whole of what makes an apply-thread frontend probe safe; behind an
  unbarriered record the allocator's free list and lifetimeId -> slot map are
  moving under the read, so the value is wrong by construction - there is no
  "count it" arm and MOBILEGL_IPC_STRICT_ERRORS is not consulted.
- MGPipeApplierIsUnbarrieredApply() and
  MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply() are the non-allocator
  half of the same rule, for the surfaces that answer from a frontend SharedPtr
  without calling the allocator (SlotTables.h's state note and its reader).
- Ruling 12: MGPipeReverseAnnouncementScope -> MagmaP7AllocatorDebtScope, keyed
  on MG_Config::ActiveBackendType == DirectVulkan. Magma's four apply-thread
  allocator debts move onto it (VulkanRenderer's two hidden-resource shutdowns
  and the named-blit endpoint resolve, ResourceTracker's GPU-write
  announcement); an Espryt probe can no longer borrow Magma's exemption by
  constructing its scope.
- BufferImpl::HandleOfBuffer is Espryt's debt, not Magma's, so it moves to
  MGPipeFrontendKeyedRegistryScope - which the Magma-keyed scope would not have
  exempted at all. It retires when vi/sb carry the handle into
  EnsureBufferResourceForHandle / IsBufferDrawCleanByHandle.
2026-09-18 06:34:48 -04:00
swung0x48 5ef4701294 [Config, MG_Backend, MG_Test] (Disaggregated): P5e c0e - the two knobs and the caps arm
- IpcTable::RunAhead (MOBILEGL_IPC_RUN_AHEAD, default 1, forced 0 under PipeVerify beside
  BatchWaits) and IpcTable::PresentCredit (MOBILEGL_IPC_PRESENT_CREDIT, default 1, range
  1..8). Both parsed unconditionally like every other IPC knob and both in the IPC log
  line, because a knob that silently means nothing on one arm of an A/B is how an A/B
  stops being one.
- RUN_AHEAD=0 is an A/B CONTROL and not a negative one: identical server code, identical
  records, identical fill decision, and only the client's wait differs. VERB_BARRIER=0
  keeps its own meaning as the lockstep arm's negative control.
- MG_Backend/Init.cpp: kMGPipeP5eRunAheadReady = false, and the caps arm goes through
  MGPipeRunAheadCapBitsFor so the "DirectGLES only, and only once the integration commit
  flips the constant" decision has one spelling. A constant and not a knob: an operator
  must not be able to turn a half-migrated server into a run-ahead one, because the
  failure mode is a torn read that renders wrong rather than aborting.
- MagmaPipeIdentityTest pins both halves - no bit 10 for DirectVulkan whatever the
  integration constant says, bit 10 for DirectGLES only when it is true, and that one bit
  and nothing else.
- The push default becomes kMGPipeSubsystemsMigratedAtP5e (0x3fff). Inert: bit 13 is not
  in kMGPipeWiredSubsystems, so no emission gate opens on it; 0x1fff survives as P5e's
  "everything P4a had and nothing of mine" control.
2026-09-18 06:26:32 -04:00
swung0x48 e99796d6ec [MG_Pipe, MG_Remote, MG_Test] (Disaggregated): P5e c0e - the wait-class column and opcode 80
- PipeCalls.def gains a FIFTH COLUMN, WaitClass, on every row, and gen_pipe.py generates
  MGPipeWaitClassFor(op) beside kMGPipeCallFlags. It is a VALUE and not a new op: opcode
  order and the flag column are untouched, and the point of generating it is that the emit
  table and the sink compute the barriered predicate's static half from ONE table instead
  of two copies that agree until they do not.
- Two new generator gates, each with its own --self-test negative control: a token that is
  not an MGPipeWaitClass enumerator, and - both ways - a kReplySlot row that does not wait
  for its reply or a kWaitReply row that owns no slot. The first would be a waiter that
  hangs, the second a block on an answer nobody posts. Twelve controls trip now, up from
  nine.
- set_program_bindings appended as opcode 80 (kCtxState, kVarTail|kHostSpan): the three
  POST-LINK mutable reflection fields, which live inside the LinkArtifacts the archive
  snapshots and all move after the link, so no existing row can carry them. It lands with
  a NULL ROUTE and a sink arm that validates and declines, exactly as set_shader_buffers
  has sat catalogued-and-dead since P4a.
- The codec's record layout grows to THREE tails for it, with each declared count refused
  against its bound rather than truncated, and the third tail's name spans walked through
  CheckHostSpanIsHonest in the decode arm - they are members of a 40-byte element, so the
  encoder's blanket second-tail pass would read one span and a quarter of garbage.
- PipeCatalogueTest: the new sizes (MGPProgramDesc 200), the opcode-80 and null-route pins,
  the draw flag, and a wait-class case that re-states CONTRACT-P5E section 2.2's table BY
  NAME rather than reading the def - every other statement about the column is derived
  from the def and moves with it, so this is the second place a demoted row has to be
  argued. The ABI fingerprint moves with MGPWireOp::kOpCount, which the handshake case
  already pins.
2026-09-18 06:26:14 -04:00
swung0x48 7568161eb4 [MG_Pipe, MG_Impl, MG_Test] (Disaggregated): P5e c0e - the types, the subsystem bit and the image-access table
- kCapRunAheadApply (bit 10) and MGPipeRunAheadCapBitsFor(backend, ready): the arm is a
  pure function so "Magma never publishes it" is a unit case rather than a line only a
  live split session reaches. Espryt's half is gated on the integration constant.
- MGPProgramBindings (opcode 80's head) plus its two tail element types, the three
  declared bounds, and MGPProgramDesc::LinkStatus. The descriptor grows 192 -> 200: its
  four trailing Uint8s end exactly at offset 24 and MGPBlobRef is 8-aligned, so the
  draft's "in the descriptor's existing pad" was wrong and there is no spare byte. The
  payload is instantiated only under MOBILEGL_PIPE_PUSH, so G1 does not see it.
- kDrawClientArrays (bit 6) and kMGPipeMaxBufferBindingPoints (84). The draw flag is what
  makes a client-memory vertex array visible on the wire; it is refused under run-ahead
  by vi, staged by P8, and the lockstep arm is unchanged.
- MGPipeImageAccess + MGPipeDecodeImageAccess in MGPipeValueTypes.h, and the client
  encode in ImageEmit.h moved onto those enumerators instead of literal 0/1/2. One table,
  both roles: the server's open-coded reverse mapping and its "the encoding does not
  exist" comment were the drift this closes. The encode is now a free function so the
  unit case can reach both halves.
- MGPipeWaitClass, the enum PipeCalls.def's fifth column spells; kMGPipeSubsystemBufferBindings
  (bit 13) with kMGPipeSubsystemsMigratedAtP5e = 0x3fff; the three suppressor slots for
  set_shader_buffers, one per Class, so an emission of one class cannot cancel another's.
- Tracker.h maps dirty bits 15/16/17 onto bit 13 - inert, because the emission gate also
  tests kMGPipeWiredSubsystems and the family's wired constant is still 0 - and
  TrackerTest follows the phase constant, which is now the whole bit set.
2026-09-18 06:25:54 -04:00
swung0x48 2fde70341d [Docs] (Disaggregated): P5d round 3 close-out - the report, the measurements section and the roadmap row
- P5D-INPROC-PERFORMANCE.md: the round-2 diagnosis is corrected against the
  symbolized call graph (the apply thread's mutex time was the idle poll, not
  the backend pendingMutex), the four round-3 packages and the two race fixes are
  described, the numbers table carries the two device caveats (monolith is
  120 Hz vsync-capped in most runs; the kernel ignores thread affinity, so the
  client thread runs on a mid core), and the lockstep-feasibility research says
  why the deferred draw barrier waits for P3b/P4b and P11.
- MEASUREMENTS.md section 9: the per-arm table with per-thread CPU ms/frame as
  the primary metric, the interleaved session and the wait/park counters.
- ROADMAP.md: the P5d row is the three-round shape; P11 gains the gPipeInputs
  versioning item the research points at.
- CURRENT_STAGE_PROGRESS.md, README.md, ARCHITECTURE.md (env table: the
  generate_mipmap exception to batched waits, the calibrated spin, the affinity
  readback): head 1f8de61b, the gate numbers and ruling ID-79.
2026-09-18 04:30:38 -04:00
swung0x48 1f8de61be1 [MG_Remote, MG_Pipe, MG_Impl, MG_State, MG_Backend, MG_Test, MG_IntegrationTest] (Disaggregated): P5d round 3 - the profile-directed cuts on both roles
- The round-2 report guessed the apply thread's ~48% mutex time was the backend
  pendingMutex; the symbolized call graph at 56a77348 says it is the idle poll in
  ServerLoop::ApplyThreadMain taking m_controlMutex twice per iteration
  (ControlIsPending in the ready lambda, PumpControlRequest), with DrainRing at
  29% of the thread. Every package below is cut from that graph, not from self%.
- T (transport): the control mailbox gets an atomic shadow bit m_controlPosted so
  the idle poll takes no lock (handshake, C2 critical section and the
  NOT_INITIALIZED answer unchanged); Doorbell::Wait spins on a once-calibrated
  iteration budget and reads no clock in steady state (the park phase alone
  computes deadlines); client/server wait and park counts ride the stats line
  as wait[srv= srvpark= cli= clipark=] and the wire ledger as cliwait=/clipark=.
- D (role guards): OnApplyThread() is inline - one relaxed atomic key against the
  caller's thread pointer (aarch64 __builtin_thread_pointer, else
  std::this_thread::get_id) - and m_applyThreadId, the function-static guard and
  the pthread_equal call are gone; the three scope depth counters stop being
  thread_local (their only reader is the apply-thread guard, so they count only
  when OnApplyThread() is true; a GL-thread scope no longer exempts the apply
  thread, pinned by a new case); the guard chains are ordered cheapest-first.
- B (persistent maps): the shadow allocator rounds the SIZE to a page multiple, so
  TrackWriteMap aligns OUTWARD when the shadow is page-granular - a sub-page map
  is one protected page, edges and their per-draw XXH3 disappear; the inward arm
  stays as the fallback. Review found a hole the edge hashes had hidden since
  round 1: a map that faulted but was not consumed by the walk that advanced the
  epoch was never pushed. The walk now drains every marked member before the
  binding walk, registration bumps the epoch so a fresh map ships once on the
  draw path, and a non-4 KB kernel page refuses the mprotect arm by name.
- C (client per verb): an image-unit high-water mark (push build only, fed by
  glBindImageTexture) replaces the 192-unit sweep per draw; the residual fill's
  7-term supplied conjunction is a memo keyed on its four inputs; the server's
  63-field stamp per verb is a constexpr per-class mask table.
- generate_mipmap no longer skips its wait under MOBILEGL_IPC_BATCH_WAITS: its sink
  is the backend GenerateMipmap, which reads MGB_CTX->GetActiveTextureUnit(), and
  a batched publish let the next validate move the serial under it -
  Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@ReadPixels"} on the gate tree.
  The rule is written at the site: a record may skip its wait only if its apply
  reads nothing the residual fill writes. The texture-death scenario gets the
  framebuffer case's fence before it reads the object_death tally.
- Redmi 2f7cbe2e, FCL + Minecraft 26.3-rc-3, VD12, CPU pinned: inproc 64.3 fps
  (client 15.0 ms/frame, apply 12.4) -> 103-114 fps (client 8.7-9.2 ms/frame on a
  mid core, apply 7.0-7.4); monolith 115 (120 Hz vsync-capped runs) / 206 (one
  uncapped run, 5.0 ms/frame on the big core). Gates: unit 2203/2203,
  integration-split 111/111 (the two formerly flaky cases 6/6 on repeat),
  generators, include closure, dirty surface and doc citations green.
2026-09-18 04:28:25 -04:00
swung0x48 85362a856d [Docs] (Disaggregated): P5d close-out - the inproc performance report and its roadmap row
P5D-INPROC-PERFORMANCE.md states what the phase set out to do (inproc frame
rate comparable to monolith at VD12 without touching wire semantics or the
correctness gates), what the two landed commits did (cb06538c, 56a77348:
7-13 -> 63-65 fps against monolith 110-118, pmap 25 MB/frame -> 0.23
MB/frame), and what is explicitly not done - the backend sync mutex complex
(~48% of the apply thread's own time), the R-1 serialization (P11's
versioned inputs), the emutls/set-hash small items, and the verification
debt (79-trace set, Redmi retest window, arm64-only design note, the
uncommitted FCL-side test plumbing).
2026-09-18 02:29:36 -04:00
swung0x48 56a77348f6 [MG_Remote, MG_State] (Disaggregated): inproc performance, round 2 - every persistent map is tracked, and the consumer walk goes away in steady state
Follow-up to the performance package (cb06538c): on device the epoch skip
never engaged, because the workload always has sub-page persistent maps and
any untracked (hash-arm) member vetoed it - the full binding walk ran at
every draw (~8% of the client thread at VD12), and the direct-scan
alternative measured strictly worse (41-43 vs 58-62 fps: per-draw whole-range
scans of every untracked member plus 64-slot tracker lookups per member).

The two-part answer, after the membership map started caching the tracker
slot pointer in the parent commit:
- a sub-page range no longer falls back to the hash arm: it registers with an
  EMPTY interior (no protected pages, no bitmap) and hasEdges set, so its
  whole range is served by the two bounded edge hashes instead of vetoing the
  skip for every buffer in the process;
- the shadow's allocation alignment is a full page
  (SHADOW_ALLOCATION_ALIGNMENT = 4096, GL_MIN_MAP_BUFFER_ALIGNMENT still
  answers 64): every page a shadow occupies is wholly its own, so the
  tracker can protect the interior without ever covering a neighbour
  allocation's byte, and edge spans - and the zero-page shape itself -
  virtually disappear from real workloads. Monolith shares the allocator and
  is unaffected (110-118 fps A/B, no behaviour-visible change).

VD12 Minecraft, same scene: 63-65 fps (parent commit 60-66, with the skip now
engaging every steady-state draw rather than never). GetBindingPoint and the
binding walk no longer appear in the profile at all; XXH3 drops 6.1 -> 2.6%
(the rest is the set-record dedup hash, a pre-existing P5 site). Lanes:
unit 2188/2188, integration-split 110/111 with the one red again the
monolith-inert GenerateMipmapDepthPixels environmental flake.
2026-09-18 02:21:08 -04:00
swung0x48 cb06538c06 [MG_Remote, MG_Pipe, MG_Impl, MG_Test, MG_IntegrationTest, Docs] (Disaggregated): inproc performance package - dirty-driven persistent-map push, batched barrier waits, spin-clock batching
VD12 Minecraft on the Redmi (FCL, DirectGLES): the split's in-world frame rate
13 -> 60-66 fps against monolith's 110-114, and persistent-map wire bytes
25 MB/frame -> ~0.23 MB/frame. The residual split-specific costs are named at
the end, not hidden: the R-1 serialization (WaitForApplied ~12%, P11's
versioned-inputs design) and a split-only mutex complex still under
investigation.

Persistent-map push, three arms picked per buffer at map time:
- mprotect dirty tracker: the mapped range's page-aligned interior is
  protected PROT_READ; the app's next write faults, the SIGSEGV handler marks
  the page in the slot's bitmap and unprotects, and the push ships one 64 KB
  block per block containing a faulted page. Ownership is three tests -
  SEGV_ACCERR only (a userspace raise and an unmapped page chain verbatim),
  inside a live tracked range, and not a refault on a still-set bit (a write
  we unprotected succeeds, so that is a non-write access and it crashes
  honestly). The alignment is INWARD: an outward-aligned edge page holds
  foreign bytes, and a signal-blocking worker thread faulting on one is a
  process kill - that was the ctest-only SmallRing segfault. Edge spans
  (at most two per map) are hashed per push and ship their containing block
  only on change (unconditional edges measured 112 MB/frame). Re-arm is one
  mprotect per contiguous run. The first push ships everything once.
- hash-suppress arm (MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS, default 1):
  per-block XXH3 against the last push's hashes; untracked buffers only.
- PushDrawConsumers: only the buffers the upcoming draw/dispatch can read
  (VAO attribs/element, indexed binding points, indirect/parameter slots,
  TexBuffer backing - the last also retires the per-frame-cadence cloud
  twitch), plus the fault-epoch skip: an unmoved epoch proves no interior
  byte changed, tracked edges are hashed in place, and any hash-arm member
  vetoes. The membership map caches the tracker slot pointer (a 64-slot
  rescan per buffer per draw was measured and removed).

MOBILEGL_IPC_BATCH_WAITS (default 1; MOBILEGL_PIPE_VERIFY forces 0): a
value-class record (kCtxState/kCtxCso/kCtxObject with no reply slot) is
published without waiting for its own apply; the barrier is taken at the next
pull-reading verb, the only place BARRIER-PULLED fields are read, so the R-1
fence posts are untouched and only the round trips between them change. Two
consequences are pinned rather than argued: kCtxObject verbs the tests read
synchronously get explicit fences (GenerateMipmapFieldsCross,
FramebufferDeathCrosses), and a frontend object whose last SharedPtr drops on
the apply thread - impossible before batching - now enqueues its
(kind, lifetimeId) on the deferred destroy queue and the GL thread replays
the DestroyAndFree helper at the verb hooks (CopyImageLayeredScenario aborted
Fatal{RoleViolation, MGPipeSlots} without it).

Doorbell::Wait's spin reads the clock once per 64 yields: on the workload
device a steady_clock read is a real syscall (no vDSO) and the per-iteration
reads were ~20% of the whole process at ~1000 waits/frame.

Gates: gen_pipe --check/--self-test green; unit 2188/2188;
integration-split 110/111, the one red being GenerateMipmapDepthPixels whose
DirectGLES and DirectVulkan MONOLITH siblings flake identically with every
code path of this package provably inert (PushIsArmed false, no ClientSession,
deferral's monolith early-out).
2026-09-18 01:58:59 -04:00