[MG_Impl, MG_Remote, MG_Test] (Disaggregated): P5e ra2 - a barriered fill must be quiescent, not merely about to park

- ID-132's finding, diagnosed: with the flip thrown the 70 red lane entries are ONE race
  and two debts. The race is the client's residual fill writing gPipeInputs while the apply
  thread is inside an earlier UNBARRIERED record. Measured interleaving, verbatim:
  `[MobileGLIntegra/ERROR] RA2-PROBE fill verb=DrawElements wireop=81 waitclass=-1
  applierInside=1` then `[mgl-srv-apply/ERROR] RA2-PROBE unfresh field=IsCapabilityEnabled
  verb=DrawElements serverStamped=0 ownership=RECORD-SUPPLIED` then
  `[mgl-srv-apply/FATAL] MGPipe: Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@DrawElements"}`.
  The GL thread's MGPipeServerClearVerbBoundary withdrew the applier's own stamp mid-record.
- The marker's verb is the CLIENT's verb, which is what identifies the writer without a
  probe at all: the applier can only ever put a verb of MGP_VERB_OP_LIST into the block, and
  40 of the 54 unbracketed aborts named a verb that is not in it.
- A/B control on the same binary and the same records: MOBILEGL_IPC_RUN_AHEAD=0 is 8/8 green
  where the default arm is 2/6 red, so the defect is in the wait rule and not in a record.
- RefusePipeInputsTouchWhileApplierOwnsIt could not fire on any of it: its run-ahead arm read
  `if (isBarrieredFill) return;`, and that sentence is a claim about the FUTURE ("this thread
  is about to park behind the record"). The order at the validate point is fill, then emit,
  then park. So the arm now tests the fact - `isBarrieredFill && !ApplyThreadIsInsideApplier()`
  - and the fill sites MAKE it true first with §2.5's forced wait (QuiesceApplierBeforeFill).
- TWICE PER VERB, because the block is written in two phases that straddle publication: the
  serial bump / stamp withdrawal / verb rename run before the fourteen emitters and the
  63-field walk of step 4 runs after them, and the records those emitters published are
  records whose apply reads the block.
- ClientVerbIsBarriered: MGP_VERB_OP_LIST joins the whole draw family through one row
  (DrawVbo -> DrawArrays), so inverting it verb-first answered kOpCount for DrawElements and
  eighteen other draw verbs, and the "unknown verb answers barriered" default filled for each
  while the record it published was a kWaitNone draw_vbo it never parked behind. The family's
  row is applied to the family; every other unknown verb keeps the conservative answer, which
  the wait above now makes honest. Measured alone it is worth 112 -> 133 isplit; with the wait
  in place the lane is 161 either way, so this half is the PERFORMANCE half - without it every
  indexed draw takes a forced drain and run-ahead is lockstep by another name on the draw path.
- Red-once A: put the guard's arm back to `if (isBarrieredFill) return;` and
  RemoteGuards.ClientBarrieredFillWhileTheApplierIsInsideUnderRunAheadIsFatalByName exits 0
  instead of aborting (`Value of: DiedOfAbort(child) Actual: false`), while its green control
  keeps passing. Red-once B: delete the kDraw fallback and
  RemoteRunAhead.AnIndexedDrawVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll dies of signal 6
  with `Fatal{RoleViolation, "gPipeInputs"} - the GL thread touched gPipeInputs
  (MGPipeValidateForVerb) on a RUN-AHEAD session in a barriered fill that did not first wait
  for the applier to catch up`.
- Both new cases run in the ordinary unit lane with run-ahead armed by the fixture, which is
  what ID-132(1) asks for: the strict lane runs under lockstep and cannot see this class.
- Gate on the flipped head: unit 2256/2256, isplit 161/181 (was 112), gpu 1339/1359 (was 1290),
  gens rc=0, flavours rc=0. The remaining 20 are two named debts, not this race: 18 are
  `GetBufferBindingSlot@DrawArrays [UNBARRIERED]` on the two indirect multi-draw tiers, whose
  retiring phase is P8, and 2 are ID-82's own refusal of client vertex arrays.
- The arm is stated and not inferred (ID-81): WaitForApplyToCatchUp's first test is
  `m_runAheadArmed && m_started`, which reduces to Transport != Monolith. Under lockstep,
  under monolith and under MOBILEGL_IPC_RUN_AHEAD=0 every line added here is a call that
  returns, so those arms are unchanged.
This commit is contained in:
2026-09-18 13:38:40 -04:00
parent 985ae62dfa
commit 2d03565f87
4 changed files with 209 additions and 10 deletions
+32 -6
View File
@@ -1316,14 +1316,40 @@ namespace MobileGL::MG_Remote::Client {
//
// This is where a fill left standing for an unbarriered record lands, by name.
if (ClientSessionInstance().RunAheadArmed()) {
if (isBarrieredFill) return;
// ---- P5e (ra2): THE EXEMPTION IS A FACT NOW, NOT A CLAIM ------------------------
//
// This arm used to read `if (isBarrieredFill) return;`, i.e. the caller's own
// sentence "this touch is the residual fill of a record this thread is about to park
// behind" was accepted as the whole argument. It is an argument about the FUTURE.
// The order at the validate point is fill, then emit, then park, so at the instant
// of the write the apply thread is still draining the unbarriered records the client
// ran ahead of - and the measured consequence was the GL thread bumping
// CurrentVerbSerial, withdrawing MGPipeServerClearVerbBoundary's flag and renaming
// m_currentVerb underneath a record the applier was inside, which surfaced as
// Fatal{UnmigratedPipeInput, "<field>@<the client's verb>"} on the apply thread.
// Because the exemption was unconditional, this guard could not fire on any of it:
// that is the answer to "does RefusePipeInputsTouchWhileApplierOwnsIt fire, and if
// not, why not".
//
// So the sentence still exempts the fill, but only once it is TRUE of this instant.
// The fill sites establish it by taking §2.5's forced wait first
// (QuiesceApplierBeforeFill, PipeFill.cpp); removing that wait is the red-once and
// lands here, by name, on the first barriered verb behind a run-ahead backlog.
//
// ApplyThreadIsInsideApplier() is an acquire load of the flag PipeApplier raises for
// the whole of ApplyOne - stamp, decode and LeaveApplier included - so "not inside"
// is exactly "no record is being applied" and not merely "not decoding".
if (isBarrieredFill && !ApplyThreadIsInsideApplier()) return;
if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block
MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched "
"gPipeInputs (%s) on a RUN-AHEAD session outside a barriered fill. With "
"the client running ahead the block is the server's to read for as long "
"as an unbarriered record is in flight (CONTRACT-P5E §3), and this thread "
"has no wait that would make the touch quiescent",
surface);
"gPipeInputs (%s) on a RUN-AHEAD session %s. With the client running ahead "
"the block is the server's to read for as long as ANY record is in flight "
"(CONTRACT-P5E §3), so the only legal GL-thread write is a barriered fill "
"that has first taken §2.5's forced wait (QuiesceApplierBeforeFill)",
surface,
isBarrieredFill ? "in a barriered fill that did not first wait for the "
"applier to catch up"
: "outside a barriered fill");
std::abort();
}
// MOBILEGL_IPC_BATCH_WAITS=1 makes "the fill runs while the apply thread applies an