Commit Graph
3176 Commits
Author SHA1 Message Date
swung0x48 985ae62dfa [MG_Backend, MG_Pipe] (Disaggregated): P5e - throw both run-ahead constants (NOT FOR MERGE YET)
kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp) and kMGPipeP5eClientWaitRuleLanded
(MG_Pipe/PipeApply.h) both to true. They are ONE switch with two spellings and the integration
commit throws both.

THIS BRANCH IS NOT MERGEABLE AS IT STANDS and is parked so the defect it exposes can be worked on
against a reproducible head. Measured on this commit:

  build rc=0 · unit 2254/2254 · gens rc=0 · flavours rc=0 (pull 915 TUs, push 929 TUs)
  integration-split   112 / 181   (69 failed)
  integration-gpu    1290 / 1359  (69 failed)

and the failures are FLAKY: 71 failed at -j 4, 68 at -j 1, and a single entry run alone passes and
then aborts on a repeat. A census over all 181 entries under each entry's own command and
ENVIRONMENT gives 70 red on 17 distinct <field>@<verb> pairs, over just FOUR fields:

  27  GetRenderStateParametersVersion
  25  GetTextureContextId
  16  GetBufferBindingSlot
   2  IsCapabilityEnabled

spread across eleven verbs - Clear, DrawArrays, DrawElements, DrawElementsBaseVertex,
DrawElementsIndirect, DrawElementsInstancedBaseVertex, EndTransformFeedback, GetIntegeri_v,
GetSyncStatus, MultiDrawElementsBaseVertex, ReadPixels. Several of those verbs are BARRIERED, which
is what makes this interesting: 16 of the 70 abort on the UNBARRIERED arm
(why=UNBARRIERED) and the other 54 are the plain unfresh-read abort with no [BARRIER-PULLED]
bracket at all, i.e. a RECORD-SUPPLIED field that the freshness stamp says was never filled.

One field at one verb, consistently, would be a missing migration. Four fields scattered over
eleven verbs, flaky run to run, including barriered ones, is a RACE - and the obvious candidate is
that gPipeInputs is shared mutable state whose mutual exclusion WAS the lockstep. The client still
fills it for barriered verbs while the apply thread is draining earlier unbarriered records, so the
client's fill and the server's verb-boundary stamp can now overlap in time. That is consistent with
P5d's own research conclusion, recorded in the roadmap, that retiring this barrier needs
gPipeInputs VERSIONING or double-buffering, deferred to P11 after P3b/P4b's handle-keyed twin
tables.

The strict lane being hard green (181/181, zero Fatal) is necessary and NOT sufficient, and this
commit is the proof: strict runs under lockstep, where ApplyOne stamps every record barriered, so it
cannot see anything that only happens when the client stops waiting. The lane measures remaining
DEBT; it does not measure whether run-ahead is correct.
2026-09-18 12:58:58 -04:00
swung0x48 72bc4268ff [MG_Test] (Disaggregated): P5e wave 3 - the strict lane's expected-marker set, re-measured on the merge
The file carried gl's base measurement (7c6f6886, before pa and mv landed) and said so rather than
hiding it. The merge discharged that note exactly as written, and the two-sided ratchet named both
deltas by itself rather than being told:

- GetProgramForDraw@DrawArrays no longer appears - pa retired it, 62 entries to 0, and it was the
  read that fired once per draw and kept every draw barriered. Removed here, in the wave that
  retired it, which is the whole point of the other side of the ratchet: an expected set that keeps
  rows nothing writes any more is how a lane rots green.
- GetBufferBindingSlot@DrawArrays is new, 18 entries, all of them the two indirect multi-draw
  tiers. mv NAMED it as a latent row before it could fire - BoundDrawIndirectBufferId's
  GetBufferBindingSlot(DrawIndirect), which "would surface as GetBufferBindingSlot@DrawArrays if an
  indirect tier were ever forced under strict" - and then gated the five tiers, which forced it.
  It retires in P8 (FieldOwnership.def:78), so it is a debt owed by a later phase and not by this
  one. Added with that reason.

Measured on the integration tree, two independent instruments agreeing (Harness/split_log_paths.py
markers, and a separate per-entry census that drives each entry's own command and ENVIRONMENT from
ctest --show-only=json-v1):

  integration-split      181 / 181 green
  Fatal{UnmigratedPipeInput   0 distinct pairs
  Admitted{                  10 distinct pairs - 8 by the generated table, 2 by escalation

So "the strict lane is hard green" is now a statement a script evaluates, not one a reader argues:
every entry completes, no Fatal marker over the log set ctest itself declares, every Admitted marker
checked against the same table the C++ answers from, and the ratchet fails in BOTH directions.

Gate on this head: build rc=0, unit 2254/2254, integration-split 181/181, integration-gpu 1359/1359,
gens all rc=0, flavours rc=0 (pull 915 TUs push-absent, push 929 TUs push=1).
2026-09-18 12:50:06 -04:00
swung0x48 97a6a46c0d Merge branch p5e/gl into feat/disaggregated (P5e gl) 2026-09-18 12:42:54 -04:00
swung0x48 e5b74028b5 [Docs] (Disaggregated): P5e ID-120 - contract amendment 8's sticky-forward clause is marked UNLANDED
- CONTRACT-P5E section 8 item 8 says GetBufferBindingPointCount's sticky forward becomes FATAL
  under split. It is not in the code and must not be put there this phase: FieldOwnership.def:147
  (field row) and :179 (forward row) both say BARRIER_PULLED, and the generator refuses a
  field/forward pair that disagrees - pinned by
  FieldOwnershipTest.TheSevenStickyForwardsAgreeWithTheirFieldRows.
- Landing it as written would either trip the generator or drag the FIELD row to FATAL with it,
  and that second reading moves 23 pairs of the derived admitted set: the row's retiring phase is
  "P7/P13", which is exactly what admits it on every stamped verb under ID-125, and a FATAL row is
  admitted nowhere - so 23 reads this phase never promised to migrate would start aborting.
- MEASURED, NOT INHERITED: ID-120 said 10 pairs; that count predates ID-125's retiring-phase
  disjunct. `--print-admitted | grep -c '^GetBufferBindingPointCount@'` says 23 on this head, and
  the note records the command as well as the number.
- Annotated in place, struck through with the phase that lands it (P7/P13), for ID-105's reason:
  a contract clause that is arithmetically impossible against the code is a clause that changes,
  and the divergence is better stated than discovered from a red gate two phases from now.
2026-09-18 12:33:04 -04:00
swung0x48 664cbf3ef8 [MG_IntegrationTest, CI, Docs] (Disaggregated): P5e - Magma's named-blit entries get a lane of their own
- Both DirectVulkan.Split.NamedBlit entries carried LABELS "integration-split" and abort under
  strict on GetFramebufferBindingSlot@Clear (VulkanRenderer.cpp:7648). P5e does not unbarrier that
  field on Magma - it retires there in P7 - so the Espryt lane could not go hard green with them
  in it.
- ARGUED RATHER THAN ASSUMED, because the brief allowed either reading: admitting the row instead
  is the WORSE option. FieldOwnership.def carries ONE retiring-phase string for both backends
  ("P5e (Espryt unbarriered), P7 (Magma)"), so a rule that admitted the pair for Magma would admit
  it for Espryt too - and an Espryt Clear reading the frontend's binding slot again is exactly the
  regression fb's handle arm exists to prevent. The lane split costs two entries; the admission
  would cost the gate.
- THE LABEL IS `integration-magma-split`, NOT `integration-split-magma`, and the spelling is
  load-bearing: `ctest -L` takes a REGEX, so `-L integration-split` matches a label spelled
  `integration-split-magma` as a substring. Measured with the obvious name first: both DirectVulkan
  entries were still listed by `ctest -N -L integration-split`, i.e. the split looked done in every
  listing and had not happened.
- Its CI step asserts the run is RED **and** that a private log carries the named marker, so
  "Magma is expected red" cannot decay into "Magma did not run". Measured: 0/2 under strict with
  two logs carrying Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}, and 2/2 green
  without the knob. integration-split is 117 and integration-magma-split is 2.
- CONTRACT-P5E section 7 states the split and the reason it is not an allowlist entry.
2026-09-18 12:32:39 -04:00
swung0x48 bc6e24ff30 [MG_IntegrationTest, MG_Util, MG_Backend, CI, Docs] (Disaggregated): P5e ID-119/ID-115 - the lane reads every log, counts, and has a positive control
Three measured defects in the strict lane, one commit each in spirit and one in fact because
they are the same step.

1. THE LOG SET CAME FROM A DIRECTORY. The CI step grepped MG_IntegrationTest/split-logs/, which
   holds 90 of the lane's entries. The ones it could not see were the F1. readback block, both
   NamedBlit pairs, the four Ct. entries and PersistentMapArm - precisely the readback population
   the allowlist is ABOUT, plus the only DirectVulkan entries in the lane. split_log_paths.py
   gains a `markers` mode that walks ctest's own {entry: MOBILEGL_LOG_FILE_PATH} map and
   classifies every Fatal{ and Admitted{ line. Measured on this head: 113 of 113 entries read.
   The lane is now a TWO-SIDED RATCHET - an unadmitted marker fails, an admitted pair not in
   Harness/strict-expected-markers.txt fails, AND an expected pair that no longer appears fails,
   so the lane cannot rot green by outliving its own debts.

2. THE `rsp` CHECK GREPPED FOR A COUNTER NO SPLIT ENTRY EMITS, and the pin behind it was vacuous:
   CountBarrierPull's unbarriered arm is [[noreturn]] and runs BEFORE ++g_residualPulls, so
   "rsp = 0 on unbarriered records" was true of every possible implementation. Replaced by an
   assertion where the counter actually is - one entry, DirectGLES.Split.StrictArming., with
   MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private log path of its own (a
   whole-lane env flip races under -j: the library opens that path "w"). It asserts
   rsp == 0 || rsp >= draws, which is the device evidence's shape - rsp ~= the draw count under
   inproc, 0 under monolith - so the draw path's retirement is visible as rsp ceasing to scale.

3. THE LANE HAD NO POSITIVE CONTROL (ID-115). Of the seven entries that passed strict, three were
   monolith transport - where nothing stamps a verb boundary, so the whole mechanism is
   structurally unreachable - two self-skipped, one was a death test and one was a Python check.
   Not one was a record-carrying split GL scenario, so "green" and "strict was never armed" were
   the same observation. New push-only counter `vbs` (PipeStats::ServerVerbBoundaries), counted
   at MGPipeServerStampVerbBoundary behind Enabled(), and the same entry asserts vbs > 0 on a
   frame that drew. It is deliberately NOT rsp: rsp reaches zero when the phase SUCCEEDS, so a
   control built on it would start failing on the day the debt is paid.

- Measured on this head (base 7c6f6886, before pa and mv): isplit 119/119; under strict the
  census is Fatal{ GetProgramForDraw@DrawArrays 52, GetBoundVertexArray@DrawArrays 9,
  GetProgramForDispatch@DispatchCompute 7 - exactly what pa and mv owe - and Admitted{ on ten
  pairs, three of them ADMITTED-ESCALATED. The expected-marker file carries all ten and says in
  its header that the wave-3 merge retires three of them.
- CONTRACT-P5E §7 rewritten around what is now checkable: the derived allowlist and its three
  disjuncts, the two-sided ratchet, the restated rsp pin, and the positive control.
2026-09-18 12:30:10 -04:00
swung0x48 a0836568ac [MG_Pipe, MG_Remote, MG_Backend, MG_Test] (Disaggregated): P5e ID-128 - admission gains the escalation disjunct
- The integrator's census over the merged tree left two markers nothing admitted, both
  DirectGLES.Split.ClientVertexArrayScenario: GetBoundVertexArray@DrawArrays. Disjunct 1 fails
  (draw_vbo is kWaitNone) and disjunct 2 fails (that row's retiring phase names P5e, correctly,
  for the ORDINARY draw path vi and mv retired). But these two entries pull the pair on a record
  the client IS parked behind: MGPipeBarriered escalates a draw carrying client vertex arrays
  (ID-83), ID-82 puts client arrays outside P5e entirely, and the read site
  (SyncClientSideVertexArraysForDrawArrays) aborts by its own name if it is ever applied
  unbarriered. The pull is legal; no static table could see it.
- Third disjunct: the record was barriered BY ESCALATION, i.e. wireSaysBarriered(op, payload,
  applierState) && MGPipeWaitClassFor(op) == kWaitNone. Both halves were already computed on
  every record - ApplyOne computes the predicate unconditionally for ID-103's reason - so this is
  one more bit stamped beside the barriered stamp, not new plumbing.
- It is a SEPARATE flag rather than a refinement of the barriered stamp, because the two answer
  different questions: the stamp answers "is the client parked" (the allocator and registry
  guards ask that), this answers "and for a reason the static table cannot see" (only the strict
  knob asks). Its default is false - the answer that admits nothing - which is the opposite of
  the stamp's default and deliberately so.
- The marker says WHICH disjunct: ADMITTED when the generated table answered, ADMITTED-ESCALATED
  when only the record did. That keeps the lane's comparison against --print-admitted exact
  instead of widening the static list with every pair that could ever escalate - and the tag sits
  in the slot the grammar already has for the reason, so Fatal{UnmigratedPipeInput filters are
  untouched.
- Escalation also happens to be the truer justification for GetTransformFeedbackProgram@DrawArrays,
  which ID-125 admits by phase. Both now hold; ID-125's disjunct is kept, as ruled.
- Red-once: AnEscalatedRecordsPullIsAdmittedAndSaysWhy. Dropping `&& !escalated` from the strict
  arm kills it - "FieldOwnershipTest.cpp:938: Failure / Actual: false / signal 6" with
  'Fatal{UnmigratedPipeInput, "GetBoundVertexArray@DrawArrays"} [BARRIER-PULLED,
  MOBILEGL_IPC_STRICT_ERRORS=1, retires in P5e (Espryt unbarriered), P7 (Magma)]'. Its control is
  StrictErrorsTurnsABarrierPulledReadIntoANamedAbort, which drives the identical pull with the
  flag at its default and must keep aborting.
2026-09-18 12:19:14 -04:00
swung0x48 faf19a6aaa [MG_Pipe, MG_Test, CI] (Disaggregated): P5e ID-125 - admission gains the retiring-phase disjunct
- ID-116's static rule failed on a row no package in this phase may touch. pa retired the
  per-draw program pull, which unmasked GetTransformFeedbackProgram@DrawArrays (11 entries):
  BARRIER_PULLED, on kWaitNone draw_vbo, and ruled out of P5e entirely by CONTRACT-P5E section
  5.7, with a retiring phase of "P3b/P4b (Espryt), P7 (Magma)". The rule was wrong, not the code.
- A pull is now admitted if the field is BARRIER_PULLED, the verb has a stamp row, the field is
  in the verb class's may-read mask, and EITHER the verb's op is statically barriered OR the
  field's retiring phase does not name P5e. This table is only ever consulted on a record the
  server already stamped BARRIERED - CountBarrierPull's unbarriered arm is [[noreturn]] and
  fires first - so the pull is legal by construction and the only open question is whose debt it
  is. The retiring-phase column is exactly that answer and was already in the table.
- A verb with NO stamp row still admits nothing under either disjunct: the server stamps only
  MGP_VERB_OP_LIST's ops, so any other bit would be a row nothing can exercise. 111 -> 174 pairs.
- The phase column is matched as a TOKEN, not a substring, so a "P5" or a future "P5e2" cannot
  answer for "P5e".
- All six of ID-125's survivors checked: GetFramebufferBindingSlot@ReadPixels and
  GetTextureUnitObject@CopyTexImage2D by disjunct 1; GetTransformFeedbackProgram@DrawArrays
  (P3b/P4b), GetTextureObject@CopyImageSubData (P7) and ValidateProgramName@ShaderStorageBlockBinding
  (P9) by disjunct 2. GetTextureObject@CopyImageSubData holds by BOTH, asserted separately.
  GetProgramForDraw@DrawArrays, GetBoundVertexArray@DrawArrays and
  GetProgramForDispatch@DispatchCompute stay rejected.
- Magma's GetFramebufferBindingSlot@Clear is NOT admitted and cannot be: the retiring-phase
  column is one string for both backends and it names P5e for Espryt. The DirectVulkan entries
  get their own lane label instead (item 7), which keeps the Espryt allowlist about Espryt.
- StrictErrorsAlsoPromotesTheStickyForwards is RENAMED to
  StrictErrorsReachTheStickyForwardsAndNameThemByField and restated. Not one of the seven sticky
  forwards names P5e (P7, P7/P9, P7/P13, P9), so on a stamped verb they are all admitted now.
  The claim that still has teeth is that the detector REACHES them - asserted by rsp moving and
  by the marker naming the field and its phase, so "did not abort" cannot pass for "never
  detected". The Fatal half keeps its own case next door on GetBoundVertexArray, a P5e row.
- Six new derivation controls, including one that makes GetTransformFeedbackProgram this phase's
  debt and requires it to leave the unbarriered verb's allowlist - without which disjunct 2
  could be a constant `true` and every other control would still hold.
2026-09-18 12:12:08 -04:00
swung0x48 3c13fa4b4a [MG_Backend] (Disaggregated): P5e ID-124 - the pull and push flavours compile again
Three sites, all pre-existing at 7c6f6886, all the same disease: something guarded by
MOBILEGL_BUILD_DISAGGREGATED used from code that is not. Nothing in the gate built either
flavour, so nobody knew.

- Managers.cpp: StagedTextureTargetForPipeTarget is a pure MGPipeResourceTarget -> TextureTarget
  switch with no wire, no record and no role in it, but it sat inside a nested
  #if MOBILEGL_BUILD_DISAGGREGATED while its only caller MGB_TEXPARAM_TARGET is guarded by
  #if MOBILEGL_PIPE_PUSH alone. Let out of the inner block without moving any text. The narrower
  fix - guarding the macro block too - is WRONG and was tried: that block also carries
  TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push flavour does need, and
  removing them broke it differently.
- DirectGLES.cpp:5575: the storage-block-signature clause called
  ComputeShaderStorageBlockBindingSignatureOf, declared only under MOBILEGL_PIPE_PUSH, from an
  UNGUARDED condition list. The clause is push-only in substance too - the signature it compares
  is over the override map the wire carries.
- DirectGLES.cpp BindCurrentTextures: keysMatch was declared inside
  #if MOBILEGL_BUILD_DISAGGREGATED while the ternary's else-arm and every use sat outside it.
  The legacy arm becomes a LAZY lambda, not an eager Bool: it reads currentProgram, which is
  null on the handle arm after pa, so the laziness the ternary gave for free is load-bearing
  (ID-81 / ID-110 - the arm decides, and the other arm's reads must not happen at all).

Gate: flavours rc=0 - pull 915 TUs (push define absent), push 929 TUs (push=1), both arms
asserted against compile_commands.json rather than against the flags, because the first version
of this step passed -DMOBILEGL_PIPE_PUSH=OFF beside an option that implies it and reported a
clean 982-TU "pull" build that was a second copy of the split build. The cache entry still read
OFF. Split build unaffected.
2026-09-18 12:07:45 -04:00
swung0x48 d8b9848003 Merge branch p5e/pa into feat/disaggregated (P5e pa) 2026-09-18 12:07:22 -04:00
swung0x48 b4789e8ed4 Merge branch p5e/mv into feat/disaggregated (P5e mv) 2026-09-18 12:07:22 -04:00
swung0x48 a6ddeb0816 [MG_Pipe, MG_Test] (Disaggregated): P5e ID-118 - resource_copy_region becomes barriered
- CopyImageSubData's apply reads GetTextureObject, a BARRIER_PULLED row that retires in P7, not
  in P5e. resource_copy_region was kWaitNone, so after the flip that is a genuinely UNBARRIERED
  record reading client memory: an unconditional Fatal, no knob, standing for two phases.
- The wait class moves rather than the field being special-cased into the allowlist. The
  allowlist is derived (ID-116), so an exception written into it would be a real defect kept
  quiet instead of a debt stated - and with the class moved the derivation admits the pair with
  no exception at all: 101 -> 111 pairs, CopyImageSubData joins the 12 barriered verbs.
- kWaitApplied AND NOT kWaitReply: the row posts no reply slot, so the client parks until the
  record applies. That is exactly what copy_framebuffer_to_texture - the two framebuffer-sourced
  copies, same kBlitOrCopy verb class - already does. THE COST IS MEASURED: glCopyImageSubData
  is zero per frame in the Minecraft 26.3-rc-3 scene, so the wait is on no per-frame path.
- PipeCatalogue's wait-class census moves with it, both halves in this commit: the named
  kWaitApplied set gains the row, the count goes 9 -> 10, and the kWaitNone offset 24 -> 25.
- Red-once: putting the row back to kWaitNone drops CopyImageSubData out of the derived
  allowlist (111 -> 101 pairs, 0 @CopyImageSubData) and fails
  "FieldOwnershipTest.cpp:422: Failure / resource_copy_region is unbarriered again: after the
  flip, a copy record reading the client's texture object is an unconditional Fatal and P7 is
  two phases away", with PipeCatalogueTest red on three counts beside it.
2026-09-18 12:02:02 -04:00
swung0x48 5c777b49e8 [MG_Test] (Disaggregated): P5e mv - the multi-draw tiers get gated entries of their own
- The resolved multi-draw tier is ONE process-wide read of MOBILEGL_ESPRYT_MULTIDRAW_MODE
  (MultiDraw.cpp's ResolveTierOnce) and the d1 block leaves it at "auto", which on this
  driver lands on "ext" - the one tier that asks the bound index buffer nothing. Three of
  the five sites the previous commit migrated therefore had no entry in the set the gate
  runs: ID-107's shape one level down, with the env knob in place of MG_Config::Transport.
- Five blocks, one per tier "auto" cannot reach (basevertex, multiindirect, indirect,
  drawelements, compute), prefix DirectGLES.Split.MultiDrawTier<Tier>., both labels, and
  MOBILEGL_ESPRYT_MULTIDRAW_MODE pinned as a ctest ENVIRONMENT property rather than a
  job-level export - review finding N-6's reason, and the PushMonolithArm block's precedent.
- "ext" is deliberately NOT registered: the d1 block already IS the ext lane. Every other
  tier was confirmed to resolve to ITSELF here rather than falling back to ext, so no entry
  in this set is a green that silently means "ext again".
- The filter is the tier-sensitive workload: MultiDrawScenario (minus the client-indices
  case d1 already excludes by name) plus IndexedDrawFamilyScenario's three MultiDraw cases.
  12 entries per tier, 60 in total.
- SplitLogPaths.cmake.in gives each of them a private log path. That is the rule every
  DirectGLES.Split. entry follows (SplitLogPaths.PrivateAndDistinct), and for these it also
  repairs the census blind spot: a third name segment means strict_census2.sh would re-run
  them without the very pin under test, so the marker is read where the lane wrote it.
- integration-split 116 -> 176, integration-gpu 1294 -> 1354, both fully green. Under the
  strict lane all 60 stop on GetProgramForDraw@DrawArrays, which is pa's row: PrepareForDraw
  runs before the tier dispatch, so no new marker class appears and mv's row stays at 0.
2026-09-18 11:59:58 -04:00
swung0x48 4f492edbf3 [MG_Backend, MG_Test] (Disaggregated): P5e ID-117 - the strict knob gains an admitted state
- CountBarrierPull aborted under MOBILEGL_IPC_STRICT_ERRORS on EVERY barrier-pulled read,
  admitted or not. The run's rc is therefore non-zero before CI ever reaches its allowlist
  comparison, so that comparison was unreachable code and "hard green" was impossible with the
  knob as written - the lane could not tell a debt this phase leaves standing from a defect.
- Third state, with the other two untouched: unbarriered stays an unconditional Fatal; barriered
  and NOT admitted stays a Fatal under strict; barriered and admitted logs one MGLOG_W and the
  entry completes. Admission is MGPipeBarrierPullAdmitted, generated from the three tables
  (ID-116), so there is no list here to drift.
- The marker keeps the grammar and changes only the tag: Admitted{UnmigratedPipeInput,
  "<F>@<V>"} [BARRIER-PULLED, ADMITTED, retires in <phase>]. Every filter written since P5c
  matches Fatal{UnmigratedPipeInput and therefore still means exactly "red".
- Deduped per (field, verb) per process. An 852-draw frame reaches an admitted readback row once
  per draw; undeduped, one frame writes 852 identical lines and the marker census is unreadable.
- CHECKED, NOT ASSUMED (the brief's instruction): StrictErrorsTurnsABarrierPulledReadIntoANamedAbort
  and StrictErrorsAlsoPromotesTheStickyForwards both still go red. Both stamp DrawArrays, whose
  op is kWaitNone, so neither pair is admitted.
- ID-119: the old "rsp = 0 on unbarriered records" comment at this function is withdrawn as
  vacuous - the unbarriered arm is [[noreturn]] and runs BEFORE ++g_residualPulls, so the
  sentence was true of every possible implementation.
- Red-once: AnAdmittedBarrierPullIsLoudOnceAndNotFatal is the same sticky forward as the case
  above it under a barriered verb; making the strict arm unconditional again kills it -
  "FieldOwnershipTest.cpp:867: Failure / Actual: false / signal 6".
2026-09-18 11:58:19 -04:00
swung0x48 dd89951062 [MG_Test] (Disaggregated): P5e pa - the strict lane gets four entries of its own
- MOBILEGL_IPC_STRICT_ERRORS=1 had no registered ctest entry anywhere: the phase's
  gate driver sets the knob by hand and pg's declared red-once for
  GetProgramForDraw never landed, so the two rows pa retires could come back and
  every gate would stay green. ID-109's finding, one scale down: the gap is a
  missing gated entry, not a missing case.
- DirectGLES.Split.StrictProgramArm. registers the only two scenarios that are
  strict-CLEAN after pa - one per half. MultiDrawScenario.BaseVertexDrawsReject-
  MalformedArguments prepares a draw and asserts on GL errors rather than pixels;
  AtomicCounterScenario's three cases are the dispatch half. Everything else in the
  lane still ends in a readback, whose GetFramebufferBindingSlot@ReadPixels is an
  allowlisted row only P7 retires.
- integration-split alone, and the prefix says which arm: the monolith arm of these
  scenarios is registered under DirectGLES. already and the knob is a no-op there,
  so labelling them integration-gpu would add entries that cannot fail for the
  reason they exist (R-16).
- Negative control run: with the arm reverted all four abort, two with
  GetProgramForDraw@DrawArrays and two with GetProgramForDispatch@DispatchCompute.
- Each entry takes a private log path through SplitLogPaths, for vi's reason: an
  entry that exists to carry a marker cannot share the file the marker is in.
2026-09-18 11:55:01 -04:00
swung0x48 a211756aec [MG_Pipe, MG_Test, CI] (Disaggregated): P5e ID-116 - the section 7 allowlist is generated, not hand-kept
- ra landed a hand-kept 13-row copy of CONTRACT-P5E section 7 in .github/workflows/test.yml, and
  it was wrong in BOTH directions at once: it omitted GetFramebufferBindingSlot@ReadPixels (21
  entries, the lane's largest survivor) and carried GetTextureObject@CopyImageSubData, which
  resource_copy_region's kWaitNone makes a genuine unbarriered read of client memory. Neither
  error is visible by reading the list. Both are obvious once the list is derived.
- gen_pipe_field_ownership.py now joins the three tables ID-84 already states the rule in -
  kMGPipeFieldOwnership, kMGPipeWaitClasses (generated/PipeWire.inc) and kMGPipeClassFieldMask /
  kMGPipeVerbClass (generated/PipeFillPoints.inc) - and emits kMGPipeAdmittedPullMask plus
  MGPipeBarrierPullAdmitted(field, verb): 101 admitted pairs across 12 barriered verbs.
- The three inputs are READ, never restated, so the allowlist moves with the code: retiring a
  verb's wait class narrows it with no edit anywhere, and a new BARRIER_PULLED row widens it only
  on verbs that wait. A verb with no stamp row admits nothing - only MGP_VERB_OP_LIST's ops can
  ever be the record a pull is reported against.
- --print-admitted gives the CI shell the SAME table the C++ answers from, which is what makes
  the hand copy unnecessary rather than merely corrected.
- Three new negative controls (each source renamed away must stop the script - a missing source
  yields a silently EMPTY allowlist, which reads as rigour and is blindness) and five derivation
  controls that move an input and read the allowlist back.
- Red-once: dropping the wait-class conjunct from build_admitted fails the self-test by name -
  "3 admission-derivation control(s) did not hold: unbarriering ReadPixels' op removes every
  @ReadPixels pair; unbarriering ResourceCopyRegion removes every @CopyImageSubData pair; no pair
  is admitted on an unbarriered verb (DrawArrays)" - and the new unit case fails with
  "GetBoundVertexArray@DrawArrays is admitted on a verb no barriered op stamps".
2026-09-18 11:54:28 -04:00
swung0x48 53b83469e6 [MG_Backend, MG_Remote] (Disaggregated): P5e pa - the draw and dispatch program rows leave the frontend
- PrepareForDraw's single GetProgramForDraw() and PrepareForCompute's
  GetProgramForDispatch() are the 68 red entries this package owns (61 + 7 in the
  base census; 62 + 7 once the PersistentMapArm entry is re-run under its own lane
  environment). Merely CALLING the accessor is what trips MGP_INPUT_CHECK, so the
  row retires by not making the call.
- DrawProgramFromRecords() states the arm ONCE and as a CONJUNCTION:
  ProgramHandleArm() AND TextureImpl::UnitTexturesByHandle(). The hoisted value
  serves four callees and the arm has to name every one of them - the second
  conjunct is BindCurrentTextures', whose legacy memo key and whose
  sampledTargetForUnit lambda both read a real object. The family bits are
  independent A/Bs, so the
2026-09-18 11:47:00 -04:00
swung0x48 f94752c01b [MG_Impl] (Disaggregated): P5e ID-112 - a compile-time trip wire on all seven pointer-backed residual rows
- EmittedCallSuppliesTheWholeField answers false for eight rows; seven of them are pointer-backed
  and this residual fill is Magma's ONLY source for them, dereferenced on its first draw
  (Magma is lockstep for the whole phase, ID-90). NoP4aFamilyFieldIsWhollySupplied is keyed on
  the EMITTER'S SUBSYSTEM, so it covered five of the seven: GetBoundVertexArray (P3a's,
  BindVertexElements) and GetBufferBindingPoint (sb's, SetShaderBuffers) sit outside
  kMGPipeP4aFamilySubsystems and had no compile-time protection at all.
- A package that tidied one of those two away would not break the build - it would SIGSEGV Magma
  on a device, which is the shape that already cost this phase 38 scenarios (ID-107). The seven
  are now named in one list with their own assertion, so the most likely mistake of every
  remaining package is a build break instead.
- The comment says where the fired assertion sends you: a row retires in the phase that stops
  the backend reading a frontend object (P7/P8), never in a consumer-side cleanup.
- Red-once: deleting the GetBoundVertexArray case fails the build - "PipeFill.cpp:2720:23: error:
  static assertion failed due to requirement 'NoPointerBackedRowIsWhollySupplied()': a
  pointer-backed residual row stopped being pulled (ID-112)". The pre-existing P4a assertion
  stayed silent on the same edit, which is the gap ID-112 names.
2026-09-18 11:45:31 -04:00
swung0x48 8df7861f07 [MG_Remote, MG_Test] (Disaggregated): P5e ID-111 - the barriered stamp asks the server's own capability
- ApplyOne stamped `kMGPipeP5eClientWaitRuleLanded ? wireSaysBarriered : true`, which reads a
  build constant and never asks which server this is. kCapRunAheadApply is never published on
  DirectVulkan (ID-90), yet draw_vbo / blit / clear / launch_grid are all kWaitNone - so the day
  the constant flips, a Magma server would stamp every draw record UNBARRIERED while its client
  is still lockstep. CountBarrierPull is an unconditional Fatal on an unbarriered pull, no knob
  involved, so Magma dies on its first draw and MagmaP7AllocatorDebtScope's exemption goes with it.
- The stamp is now `MGPipeApplierStampsBarriered(waitRuleLanded, serverPublishesRunAhead, wireSaysBarriered)`,
  a pure function beside the applier, so ID-90 is something a unit case can hold AT THE POST-FLIP
  value of the constant rather than a claim that first becomes testable on integration day. That
  is MGPipeRunAheadCapBitsFor's shape, for MGPipeRunAheadCapBitsFor's reason.
- Both predicates stay computed unconditionally, as arguments rather than short-circuit arms:
  ID-103's reason, extended to the capability probe.
- `ServerPublishesRunAhead` is promoted out of the anonymous namespace as
  `MGPipeServerPublishesRunAhead` (body unchanged) so the red-once can state "this session lacks
  bit 10" in its own words instead of inferring it from the stamp under test (ID-102).
- Red-once: ADrawOnASessionWithoutTheRunAheadBitStaysStampedBarriered drives a real DrawVbo
  through ApplyOne on a SetCapabilityBits(0) session. Deleting the new conjunct makes the
  post-flip assertion false - "RemoteClientControls.inc:143: Failure / Expected: true / exited 124".
2026-09-18 11:43:17 -04:00
swung0x48 98ea16042a [MG_Backend] (Disaggregated): P5e mv - the multi-draw path stops reading the frontend VAO
- MultiDraw.cpp asked the bound element array buffer three questions (is one bound,
  what GL name is bound and how big is its store, give me its bytes on the CPU) and
  asked all three of MGB_CTX->GetBoundVertexArray() on the apply thread. That was the
  last unguarded read of that field in the backend: package vi retired the ordinary
  draw path's copy and this one survived only because these 9 entries used to die
  earlier, on the framebuffer row fb has since retired.
- ResolveBoundIndexBuffer(question, entry) answers all three from
  MGPipeApplier().IndexBuffer.Res on the record arm - EnsureBufferResourceForHandle
  for the driver name, ResourceWidthForHandle for the size, the server's staged
  shadow (hostBytes + RequireStagedCoverage) for the CPU bytes - and keeps the
  monolith body token for token below it. No wire row was added.
- The arm is STATED, not inferred (ID-81 / ID-110): BufferImpl::VertexInputReadsRecords(),
  the family's own selector, which reduces to Transport != Monolith AND the vertex-input
  bit. Nothing here decides an arm from a null pointer or a noted handle.
- A missing record on the handle arm is a named refusal:
  Fatal{RoleViolation, multidraw-index-buffer-arm}, the shape of
  RefuseNullFrontendTextureOffTheHandleArm. The frontend element slot is not a second
  opinion on this side - it answers for whatever the client has bound now.
- VertexInputReadsRecords() moved from DirectGLES.cpp to Managers.h, unchanged, so the
  two translation units on this draw path share ONE selector rather than a copy each.
- Strict census: GetBoundVertexArray@DrawArrays 9 -> 0. The same 9 entries now stop on
  GetProgramForDraw@DrawArrays, which is pa's row; no other row moved, no new row.
2026-09-18 11:38:28 -04:00
swung0x48 7c6f6886ff [Docs] (Disaggregated): P5e 进度、monolith 回归与设备验证写回
- CURRENT_STAGE_PROGRESS.md 新增 2.5 节:八包落地情况、设备验证发现的 monolith SIGSEGV 及其
  根因/覆盖漏洞/修复(ID-107 -> fix1 -> ID-109/ID-110)、头 3cc4e1ec 的逐门数字、三臂设备结果。
  代码头更新为 3cc4e1ec。
- MEASUREMENTS.md 新增 10 节:主机门在 44f91c74 与 3cc4e1ec 的对比(integration-gpu 1157/1294
  -> 1294/1294 是本阶段最重要的一个数:那 137 个 SEGFAULT 当时没有任何门禁步骤会看到)、
  设备四行、以及本轮与上一轮的测量纪律四条。
- ROADMAP.md 的 P5e 行与状态行标注八包全落地与当前头。

阶段未收官:kMGPipeP5eRunAheadReady / kMGPipeP5eClientWaitRuleLanded 仍为 false,翻转门是
BRIEF-P5E 3 节(strict 硬绿)与 4 节(设备出口)。
2026-09-18 10:11:19 -04:00
swung0x48 3cc4e1eca2 [MG_Backend] (Disaggregated): P5e ID-110 - the record arm's selector is stated, not inferred
fix1's report asked for a ruling on Managers.cpp's buffer-texture backing seam, which hands
kNoFrontendBuffer to the lookup on the strength of a NOTED HANDLE and was unreachable under
Transport == Monolith only because every writer of such a note happens to be transport-gated.
That is safe by a chain rather than by its own statement, and it is exactly the shape that cost
the monolith arm 137 scenarios across 38 files (ID-107): fix1's root cause was one seam reading
"a handle was noted" as "the record arm is selected", which are different claims.

- MGB_TEXTURE_RECORD_ARM_SELECTED() names the second claim once: Transport != Monolith AND
  TextureResourceSubsystemEnabled(), the two conjuncts every null-frontend-servable read already
  lives behind (CONTRACT-P5E 5.2 / 5.8). It is (false) in the pull build, so nothing moves there.
- RefuseNullFrontendTextureOffTheHandleArm spells it instead of repeating it inline, which also
  removes its nested #if.
- texBufferByRecord gains it as a third conjunct. Under a transport it is already implied by
  `pushedStorage != nullptr`, so no behaviour changes and no door closes; the arm now says what
  it depends on instead of depending on it silently.

Gate on the integration head: build rc=0, unit 2250/2250, integration-split 116/116, gens all
rc=0 (the CONTRACT-P5.md:524 doc-citation ambiguity is the standing pre-existing one).
2026-09-18 09:57:57 -04:00
swung0x48 6662176733 [MG_Backend, MG_Test] (Disaggregated): P5e fix1 - the by-handle attachment sync must not run on the monolith arm
- FramebufferImpl::SyncAttachmentSurface selects its STORAGE sync by MG_Config::Transport
  (ID-81 / CONTRACT-P5E §5.8) instead of routing both arms through tx2's by-handle seam. Under
  a transport it is unchanged (SyncMipmapsToBackendByHandle / RBO SyncToBackendByHandle); under
  Transport == Monolith it is the frontend sync it was at f6cfcbd3, driven by a monolith-glue
  attachment pointer the object-form caller passes and the handle-form caller leaves null.
- The crash this repairs: the push-monolith build drives its framebuffer attachments from the
  record (P4a D-C2, gated on FramebufferSubsystemEnabled() alone), so glClear on an FBO holding
  a texture reached SyncMipmapsToBackendByHandle, which passes a NULL SharedPtr and relies on a
  record arm that `Transport != Monolith` selects. stateTextureObject->IsTextureView() then
  dereferenced null - the device SIGSEGV in Lightmap.<init> -> clearColorTexture -> glClear.
- The three texture sync bodies stop treating "a handle was noted" as "the record arm will be
  selected": RefuseNullFrontendTextureOffTheHandleArm is one shared check and a null frontend
  object on an arm that cannot serve it is Fatal{RoleViolation, "texture-handle-arm"} instead of
  a null dereference. It folds to the same constant in the pull build, so no call is added there.
- The renderbuffer arm is arm-selected for the same reason: its by-handle form dereferences
  nothing but drops the application-visible GL_OUT_OF_MEMORY RecordError, which the
  push-monolith build still has an application to report to.
- New lane entries (MonolithAttachmentClearScenario, three cases: a mutable texture attachment,
  an immutable one and a renderbuffer) and a DirectGLES.PushMonolithArm. registration that pins
  MOBILEGL_TRANSPORT=monolith and carries the integration-split label, so the gate covers BOTH
  runtime arms of the push build instead of only the inproc one (ID-107's coverage hole).
- Gate: build rc=0, unit 2250/2250, integration-split 116/116 (113 + 3), integration-gpu
  1294/1294 (137 DirectGLES SEGFAULTs before this commit), gens all rc=0.
2026-09-18 09:48:24 -04:00
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