Commit Graph
134 Commits
Author SHA1 Message Date
swung0x48 072ea1b832 [android-plugin, scripts] (P7): a passed APK replay whose device went offline before its logs were copied asks for the infrastructure retry 2026-09-23 13:23:09 -04:00
swung0x48 b13045918a [CI, scripts, tools] (P7): the pull control strips ctest -V's line prefix, retrace-split pins lavapipe, and the retrace summary reads role logs 2026-09-23 13:23:09 -04:00
swung0x48 f1ce655b42 [MG_IntegrationTest, scripts] (P7): the Tcp TextureUploadShape entry holds the mobilegl-tcp lock, and the lane tools refuse any supervisor entry that does not 2026-09-23 13:23:09 -04:00
swung0x48 d1549cba57 [scripts] (P7): the TCP lane supervisor starts headless like every other server the harness reaches 2026-09-23 13:23:09 -04:00
swung0x48 9fd768d493 [MG_Backend, MG_IntegrationTest] (P7): bound wire descriptor sets within long frames 2026-09-23 01:35:20 -04:00
swung0x48 1ab62ece4c [ci] (P7): the parity exception list names M2 round 2's fourth Reclaim case
- ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle rides the same split/spawn
  fixture as the three gauge cases and the tcp arm does not carry it (magma-m2.md §9.3); the
  integration tree keys this list by exact tail, so the fourth name goes in beside the three
2026-09-22 22:56:43 -04:00
swung0x48 3b8bf9ec25 [scripts] (P7): the pull-library control refuses an empty grep -F pattern in EVIDENCE, and the stub ctest is executable again
- every line of EVIDENCE is a grep -F pattern and an empty line matches everything: a trailing
  newline or a blank line between the two sentences would turn hole 2 into "any red will do".
  The smoke's "red without the transport-resolution message" case catches that (22/23), but
  only when somebody runs it; a `case` after the assignment now refuses a blank line or a
  leading/trailing newline with ::error:: and rc 1, before the selection is counted or the
  library is swapped. The red-check's perturbation line is untouched; smoke 23/23
- scripts/ci/testdata/stub_ctest.sh went 100755 -> 100644 in the round-2 commit that added
  its retrace-evidence-nolog mode (a Windows-mount artefact); restored with
  `git update-index --chmod=+x`. The smoke copies it and chmods the copy, so nothing ran
  differently
2026-09-22 22:03:23 -04:00
swung0x48 bd849b2821 [tools] (P7): the audit's block walk is fail-open only for the disaggregated guard, and an indented #else stops it
- stepping over `#endif` accepted a log that is compiled out: `#if 0` / `MGLOG_W(...)` /
  `#endif` / `Count(...)` read as logged, and so did a log under `#ifdef
  MOBILEGL_VERBOSE_DECLINES` - a Count that counts every time and logs in no shipped build
- the walk now counts the `#endif`s it crosses; a same-indent log met with one outstanding
  counts only if every opener enclosing it, reached by continuing upward, is the whole-line
  `#if MOBILEGL_BUILD_DISAGGREGATED` (`&& X` is not it). `#if`/`#ifdef`/`#ifndef` with no
  `#endif` outstanding is the Count's own guard and is still stepped over, so the column-0
  `#if/#endif` fixture stays green; the comment says the walk is fail-open for that one
  guard and nothing else
- `#else`/`#elif` stopped the walk only at column 0, as a line of lesser indentation; one at
  the Count's own indentation walked straight through to the log above it. PP_STOP now
  returns False at any indentation, before the indent test
- two red fixtures, both green on the round-2 audit: "log only under a #if 0 above the
  Count" and "log only on the other side of an INDENTED #else at the Count's indentation";
  16/16, the tree stays 53 / 53 / 0
2026-09-22 22:03:23 -04:00
swung0x48 8c8fc31952 [MG_Remote, MG_Test, ci] (P7): the token compare runs a fixed 64-byte width, an ignored window ask is logged once, a nonce that could not be minted is refused by name, and a garbage first frame while busy is MalformedHello rather than Busy
- AuthToken.h: ConstantTimeTokenMatch loops kAuthTokenCompareBytes (64) iterations with both operands read as zero past their ends - its trip count no longer tells a peer the configured token's length; a token longer than 64 is still compared to its last byte (SocketTransportTest pins the 70-byte case)
- ServerSession.cpp: an ask in Hello.linkTerms above the granted terms logs one MGLOG_W naming both sides ("the ask is ignored and the session is server-sized"); the production client asks for nothing, so a session that was never mis-configured never logs it; SessionHandshakeTest's 64 GiB case requires the line (red before: absent)
- ServerSession.cpp: a MintNonce failure sends Refuse{LinkTerms} "stream data plane unavailable: the server could not mint a data nonce" before returning the fault it is (still exit 67, still counted) instead of a bare close
- ServerMain.cpp: a first frame with the wrong magic or over 1 MiB while a session is live is Refuse{MalformedHello} "first frame is not a control frame", drained before the close so the refusal is not RST away; Busy stays the answer for a well-formed frame that is not a DataBind
- tcp_supervisor_smoke.py: garbage_first_frame_while_busy (magic XXXX on a live session -> code 8; red before: code 7)
- fatal census unchanged: 79 abort sites, 3 refusal words (the new refusals go through the enum, not a literal)
2026-09-22 21:54:46 -04:00
swung0x48 c69d8dfdc8 [MG_Remote, MG_Test, ci] (P7): sha256(protocol.fbs) is pinned to MOBILEGL_PROTOCOL_CONTROL_REVISION, so a control-schema edit without a bump is red in the unit lane and in flatc-check instead of a silently absent field
- the revision is a hand-bumped integer mixed into wireFingerprint and nothing tied it to protocol.fbs's content; scripts/ci/protocol_revision_pin.py holds {revision -> sha256(protocol.fbs)} in protocol_revision_pins.json and refuses a schema whose digest is not the current revision's, or a revision with no row
- registered as ProtocolSchema.RevisionPinsDigest (label unit, MG_Test/Wire/CMakeLists.txt, --self-test proves both perturbations red before it says green) and as a step of CI's flatc-check job, the one that regenerates the header on every schema edit
- --write records a bump's row and refuses to re-pin an existing revision to a different digest, which is what a schema edit without a bump looks like
- mg_protocol_base.h says where the pin lives beside the revision it pins
- red-once: protocol.fbs touched with one trailing comment, revision left at 1 -> "protocol.fbs has sha256 a7b5c7fb... but MOBILEGL_PROTOCOL_CONTROL_REVISION is still 1, which is pinned to 7fc859f7..." rc=1; restored, green
2026-09-22 21:54:46 -04:00
swung0x48 0293aebb17 [MG_Remote, ci] (P7): a bound session closes its data-connection source, so a later DataBind is refused by name instead of queued into a child that stopped reading, and the single-session listener closes with it
- ServerMain.cpp: RunSession takes the descriptor its data-connection source reads from (the child's hand-off end under --serve, the listener itself without it) and closes it the moment Accept returns; the source object goes with it
- the supervisor's hand-off send is MSG_DONTWAIT (FdPassing::SendFd dontWait), so a peer that stopped reading answers at once; "peer gone" on a datagram pair (ECONNREFUSED, ENOTCONN) is an answer the caller names, not a logged fault
- before: every DataBind after the bind sat in the child's receive queue with no reply, its TCP connection held open until the session ended, and past the queue limit (net.unix.max_dgram_qlen 10 on the phone; ~288 datagrams of sender buffer on this host) the supervisor blocked in sendmsg for the rest of the session - no accepts, no Busy, no reaping
- before, without --serve: the listener stayed open after the data connection bound, so a second client or a retry completed a TCP handshake nobody would accept and waited out its 20 s connect budget instead of the ECONNREFUSED it got before PH-7 (4)
- tcp_supervisor_smoke.py: data_bind_after_bound (12 extra DataBinds on a bound session each get Refuse{Authentication} "data connection could not be handed to the live session", and a fresh control connection is still Busy) and single_session_listener (a connect after `ready` is ECONNREFUSED); connect_control retries ECONNREFUSED at startup for pair() and held_session() alike
- red-once at da313010: smoke TimeoutError on the first extra's receive; probe with qlen+2 fire-and-forget DataBinds: 288 queued silently, then the supervisor stopped accepting (fresh control: connect timed out) while still alive; single-session connect after ready succeeded
2026-09-22 21:54:46 -04:00
swung0x48 9328bdc735 [MG_Backend, MG_Util, MG_Test, MG_IntegrationTest, scripts] (P7): integration-magma-split and -spawn carry MagmaWireReclaimScenario, which reads the server's wbuf[] gauges and reds on a wire arm that keeps orphaned buffer stores until the frame ends
Rule J's red-once for M2. The server role's VkBufferManager publishes four PipeStats gauges
(push builds only, so the pull image is untouched): wire buffer records alive, and the run
maxima of live wire VkBuffers and of parked bytes, plus the watermark's forced-sync count -
maxima because the defect lived inside one frame and a swap-time sample reads after the frame
boundary's own sweep. Two cases, each inside a single frame: 1024 undrawn respecifies of one
4 KiB buffer (live peak <= 4 x wire buffers), and 48 respecify-and-draw rounds of a 1 MiB store
under the lane's MOBILEGL_IPC_WIRE_DEFERRED_MB=8 (parked peak <= budget + one store, every strip
its own colour). Red at base (ec46a550's defer path, counted): 1025 vs 4 live, 49,287,168 vs
9,437,184 bytes and 49 vs 11 live, on both arms; green after: 1 live, 9,437,184 bytes / 9 live
with 5 forced syncs. The knob's negative control (=0) reds the second case with the same
49,287,168 bytes. Inproc and spawn only: the tcp server is the lane-wide fixture supervisor,
whose environment and log the entry cannot reach - named in spawn_lane_parity.py as
MAGMA_NOT_ON_TCP (the ID-P7-14 shape), and PipeStatsTest pins the four short names.
2026-09-22 21:46:08 -04:00
swung0x48 46bda06226 [scripts] (P7): the pull-library control accepts the no-client-log sentence a pull build has produced since the per-role log rename
- retrace_pull_library_control.sh required run_trace_case.cmake's "never reported resolving
  it", the marker-search sentence. Since P6's one-log-per-role rename a pull library never
  reaches that search: it has one role and writes MOBILEGL_LOG_FILE_PATH unchanged
  (output/mobilegl.log, Log.h's pull arm of RoleLogPath), the runner reads
  output/mobilegl.client.log and stops one check earlier with "the run wrote no <client log>,
  so there is no evidence the transport ever resolved" - so the control's hole-2 check
  refused its own red and the step exited 1 on every pull-library run
- measured on this tree with the real ctest, build-linux's pull library over build-split's
  frozen split library, SPLIT_TRANSPORT=inproc: control rc 1, "::error::the split retrace
  went red (ctest exit 8) ... never says the library did not resolve the transport", the
  runner's fatal at the no-client-log check. With this change the same run passes for its
  own reason on both inproc and spawn
- EVIDENCE carries both sentences as grep -F's newline-separated patterns, so the evidence
  line the red-check perturbs is unchanged; both are the transport-identity assertion
  refusing the same library for the same reason, and neither is a loader failure, a missing
  fixture, a timeout or an SSIM drop
- stub_ctest.sh gains retrace-evidence-nolog, the no-client-log fatal CMake-wrapped across
  lines the way the real one arrives; control_smoke_test.sh expects it to PASS (23/23); the
  red-check still reds "red without the transport-resolution message"
2026-09-22 21:28:02 -04:00
swung0x48 b0d4d730ba [tools] (P7): the audit's block walk stops at a same-indent exit and steps over a column-0 #if
- a same-indent `case`/`default:`/`break;`/`return`/`continue;`/`goto`/`throw` line between a
  log and a bare Count ends the walk: control cannot flow from that log to the Count, so it
  is not the Count's log. A `case 1: MGLOG_W; break; case 2: Count` switch with its labels at
  statement indentation, and a log above a `return` with the Count under a same-indent label,
  both read as logged before
- `#if`/`#ifdef`/`#ifndef`/`#endif` lines are passed over: a log and its Count that straddle a
  `#if MOBILEGL_BUILD_DISAGGREGATED` are one block on both sides of the preprocessor, and the
  column-0 line ended the walk for no defect. `#else`/`#elif` still end it - a log on the other
  side of one is in a different build, not this one
- four fixtures: same-indent return (red), same-indent switch (red), log across a #else (red,
  and red on the previous audit too - it pins that #else still stops), Count under its log
  across #if/#endif (green; a false red before). 14/14 fixtures; the tree stays 53 / 53 / 0
2026-09-22 21:28:02 -04:00
swung0x48 576fc09a31 [tools] (P7): the audit's strip reads a prefixed character literal as a character, not as a digit separator
- L'"', u'"', U'"' and u8'"': the `'` after an identifier character was taken for a C++14
  digit separator, the `"` inside it opened a phantom string, and the rest of the line was
  blanked - a site on that line vanished and its row read as NO SITE (fail-closed: a false red)
- CHAR_PREFIX accepts a u8/u/U/L prefix before `'` the way RAW_PREFIX does before `"`, with
  a non-identifier character (or the start of the text) in front of it, so a real separator
  (0xAB'CD, 1'000) is untouched
- self-test fixture with L'"' and u8'"' ahead of two AT sites on their own lines: rc 0; it
  was rc 1 on the previous audit. The tree has no prefixed character literal today (53 / 53 / 0)
2026-09-22 21:28:01 -04:00
swung0x48 9d5f5012c7 [tools] (P7): a site's log is a statement that begins with MGLOG_W/E, not a line that mentions one
- the wire declines audit's LOGGED pattern was unanchored, so a guarded one-liner at the
  Count's indentation (`if (g) MGLOG_W("x");`, `g ? MGLOG_W("x") : (void)0;`) counted as
  the site's own log and the site read as logged - a decline counted every time but logged
  only sometimes, which is the unlogged site the check exists to refuse
- anchored to the statement start; the block walk matches the line, not searches it
- self-test fixture "guarded one-liner log at the Count's indentation" (both shapes) reds
  with the NO_LOG sentence; it was green on the previous audit. The tree stays 53 / 53 / 0
2026-09-22 21:28:01 -04:00
swung0x48 ce83651349 [MG_Remote, MG_Test, ci] (P7): a Hello asking for 64 GiB windows is welcomed with the server's own terms and its session child never reaches 64 GiB, pinned in process and on the TCP supervisor 2026-09-22 21:00:57 -04:00
swung0x48 20ab14daf8 [MG_Remote, MG_Test, ci] (P7): a TCP session's data connection is bound to its authenticated control connection by a 128-bit Welcome.dataNonce instead of by arrival order, so a reordered or stale data connection is refused by name and the wire fingerprint moves with the control schema 2026-09-22 21:00:57 -04:00
swung0x48 8fa1821ab2 [MG_Remote, MG_Test, ci] (P7): one constant-time token comparison serves both server policy sites, a configured token shorter than 16 bytes refuses the listen by name, and CI builds flatc so the TCP supervisor smoke runs and must pass 2026-09-22 21:00:57 -04:00
swung0x48 266d6a32f8 [tools] (P7): the wire declines audit carries a self-test whose fixtures must red, and the CI step runs it before the tree audit
Fable review of the B3 rework, should-fix: the audit had no negative
control, so a green run could not say it would have been red. It now takes
--self-test, which writes each inline fixture as a one-file renderer tree in
a temp dir, runs the audit on it and checks both the rc and the line the
audit must print (so a red is red for the fixture's reason):

  comment-only site (//)                -> rc 1, "has NO SITE"
  comment-only site (/* */)             -> rc 1, "has NO SITE"
  string-literal site                   -> rc 1, "has NO SITE"
  other-branch log                      -> rc 1, "with no MGLOG_W/E in its own block"
  nested-branch log                     -> rc 1, same
  log only in a /* */ comment above     -> rc 1, same
  nearest log is MGLOG_D                -> rc 1, same
  good sites (AT form, bare Count under
    its own MGLOG_E_ONCE, def row with a
    trailing comment)                   -> rc 0, "0 unlogged, 0 unknown"

8/8, rc 0. Mutants: with the strip disabled 5 of 8 fail (the four
comment/string reds go green and the good fixture's commented row is
lost); with the old 8-line window in place of the block walk 2 of 8 fail
(other-branch, nested-branch). Against the audit as it stood at aab01275,
six of the seven red fixtures passed and the good one failed.

.github/workflows/test.yml: the "Wire declines audit (B3)" step runs
--self-test and then the tree audit, the way the next step runs
trace_cases.py --self-test-transport-matrices before it computes matrices.
2026-09-22 20:49:45 -04:00
swung0x48 78269b2677 [tools] (P7): a bare decline count is logged only by a warning-or-error line in its own block, and a def row with a trailing comment is still a row
Fable review of the B3 rework, should-fix: the 8-lines-above MGLOG_W/E window
was branch-blind - a Count() in `if (x == 2) { ... }` whose only log sat in a
preceding `if (x == 1) { ... }` passed, as did a log in a nested branch just
above. All 12 bare sites in the tree are under their own line today, so this
was a gate hole, not a present defect.

has_own_log() walks upwards from the Count line (still capped at 8 lines):
blank and deeper-indented lines (a log's continuation lines, a nested block)
are passed over, only a line at the Count's own indentation can be its log,
and the first line indented less than the Count - the block's opening, a
sibling's `} else {` - ends the walk.

WireDeclines.def is now read through the same comment/string strip, and ROW
tolerates spaces inside the parentheses: `MGL_WIRE_DECLINE(Name) // note`
used to fail `\)\s*$`, dropping the row, and its real site was then
reported as naming no row.

Tree: 53 rows, 53 with sites, 53 sites, 0 unlogged, rc 0 (unchanged).
Fixtures the previous commit passed and this one reds: other-branch log,
nested-branch log (rc 0 -> 1); a def row with a trailing comment plus its
site (rc 1 -> 0).
2026-09-22 20:49:45 -04:00
swung0x48 88599fc6f6 [tools] (P7): the wire declines audit matches sites and logs on code alone, so a commented-out or quoted site is no site
Fable review of the B3 rework, must-fix: a row was "sited" by any line the
AT_SITE / COUNT_SITE regex matched, including a // comment, a /* */ block or
a string literal - `// MGL_WIRE_DECLINE_AT(GhostRow, "...");` satisfied the
audit for a row nothing counts, and an MGLOG_W inside a comment above a bare
Count() satisfied the log rule.

wire_declines_audit.py now blanks //...$ (with backslash-newline
continuation), /* ... */, "..." and '...' (raw strings R"d(...)d" and
C++14 digit separators handled) out of each source before any match. Every
character but the newline becomes a space, so the text keeps its length and
line count and each report still cites the real line.

Tree: 53 rows, 53 with sites, 53 sites, 0 unlogged, rc 0 (unchanged).
Fixtures that the previous audit passed and this one reds (rc 1): a
comment-only site (//), a comment-only site (/* */), a string-literal site,
a log only in a /* */ comment above a bare Count. The branch-blind window
and the .def trailing-comment row are the next commit.
2026-09-22 20:49:45 -04:00
swung0x48 4db17d58c6 [MG_Backend, MG_IntegrationTest, scripts] (P7): a declined multisample depth/stencil shape is decided once before any aspect runs, so an erroring blit writes nothing, and the shader resolve arm gets a flipped knob twin
- BlitWireFramebuffers runs colour first. A scaled COLOR|DEPTH blit out of a multisample framebuffer scaled and wrote its colour and then recorded INVALID_OPERATION on its depth, and an X-mirrored one mirrored its colour and left its depth alone - partial effects GL 18.3.1 does not allow. The shape decline (scale, X mirror, cross-format Y mirror) moves into DeclineWireDepthStencilResolveShape, which BlitWireFramebuffers asks for every depth/stencil aspect that would take the multisample resolve arm before any aspect runs; ResolveWireDepthStencil asks it too for any other caller. Default read/draw framebuffers are left to the lambda (a default read is never multisampled; a default draw behind one is the region Fatal, and resolving it early would move its write index).
- The X decline and the cross-format Y decline get their own MGLOG_E_ONCE sites; one site used to cover both, so after the first X decline a cross-format Y decline logged nothing.
- An empty rectangle on one side only (64x48 onto 0x48) is a size mismatch and now takes the scale decline (INVALID_OPERATION) instead of returning silently; empty on both sides stays a silent no-op.
- DirectVulkan.{Split,Spawn}.MsFlip1. forces MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1, so the shader arm min-corner rectangle under a flip runs on this host; split+spawn only and named in MAGMA_SERVER_ENV_KNOB_NO_TCP.
- spawn_lane_parity.py: every server-env-knob tail must match exactly one split key, so a dead or over-broad tail fails the gate instead of riding silently.
- The flipped-resolve case gains a COLOR|DEPTH scale leg (error, colour untouched, depth untouched) and an empty-destination leg (error, depth untouched).
- Red-once, each on split + spawn (+ tcp MsFlip.) and restored: pre-pass disabled -> all five MsFlip/MsFlip1 entries fail "the declined COLOR|DEPTH scale must not have written its colour either" (read {255,0,0,255}, want {0,0,255,255}); round-1 silent empty-side return -> all five fail the empty-destination leg (error 0, want 1282); shader arm handed sx0/sy0 instead of the min corner -> MsFlip1 fails on both arms ("768 of 768 depth values differ from 0.75", "928 of 3072 stencil values are wrong; first at (20, 4): got 255, want 99") while MsFlip. stays green; a dead ".MsFlip2." tail -> parity rc 1 "matches 0 split entrie(s)".
- The split once-sites have no lane red-once: the cross-format Y decline needs a device whose renderbuffer and texture D24S8 map to different VkFormats, which lavapipe is not.
2026-09-22 20:49:45 -04:00
swung0x48 75a64ce0da [tools, docs] (P7): the parity script keeps one server-side-knob list after the B3 rework merge, and magma-b3.md names it
The B3 rework's parity hunk and B2's round-1 hunk described the same asymmetry
under two names; the merge kept B2's compare_arms(no_tcp=) and added .StaleSerial.
to MAGMA_SERVER_ENV_KNOB_NO_TCP. One line of the package tree's other mechanism
(`not_on_arm = not_on_arm or {}`) survived outside the conflict hunks and would
have been a NameError on the first run; it goes, and the note's two mentions of
MAGMA_NOT_OVER_TCP now name the list that exists.
2026-09-22 20:25:48 -04:00
swung0x48 b0c6af3168 [MG_IntegrationTest, tools] (P7): the stale-serial red-once is registered on split and spawn only, and the parity gate names it with the other server-side knobs
MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL is read by the server. The tcp
arm's server is the lane fixture (scripts/ci/tcp_server_fixture.py),
started once from its own environment, so an entry-level ENVIRONMENT never
reaches it: DirectVulkan.Tcp.StaleSerial.* ran the unforced case under a
name that claimed the knob and could never red (review ID-P7-34, item 1).

The entry is now registered only when the arm is Split or Spawn, and
spawn_lane_parity.py grows a second named asymmetry beside
MAGMA_INPROC_ONLY: MAGMA_NOT_OVER_TCP, key prefixes the reference arm has
and the tcp arm does not by name. It comes off the reference for the tcp
arm alone, so the same entry missing from spawn is still an error. B2 adds
its own knob entries to the same tuple in its tree.

integration-magma-tcp 71 -> 70 entries (68 cases + the two fixture
entries); split 90, spawn 69 unchanged. spawn_lane_parity.py build-split
rc 0; tcp_lane_tools_test.py 6/6.

Integrator merge onto pipe: B2's round-1 mechanism (`mglItestServerEnvKnobArm` in
CMake, `compare_arms(no_tcp=)` in the parity script) already covers this shape, so
`.StaleSerial.` joins MAGMA_SERVER_ENV_KNOB_NO_TCP instead of the separate
MAGMA_NOT_OVER_TCP the package tree introduced.
2026-09-22 20:25:11 -04:00
swung0x48 b8d10d0730 [MG_Backend, tools] (P7): every WireDeclines.def row has a site and every site a warning-or-error line, and a tree-only audit fails the job when one does not
The B3 review (ID-P7-34) counted 12 of the 52 rows with no site at all -
TexViewFormat, TexNoApplierRecord, TexRecordDeadOrStale and the nine Shape*
rows were declared and never counted - and ten sites that counted with no (MGLOG_D included)
line, so "a name, a tally and a decline line" did not describe the tree.

- SyncWireTextureShape: all nine Shape* exits now count; the ViewOf exit
  and the preserve-flush exit were bare `return false` and are now
  MGL_WIRE_DECLINE_AT; the image-flags exit logged only MGLOG_D, which a
  Release build compiles away, and is promoted likewise.
- The preserve-copy exit was the one Shape exit with no row: appended as
  ShapePreserveCopyFailed (rows are append-only), 53 rows.
- SyncTextureResourceByHandle / UploadPendingWireLevels: the count-only
  sites without a log of their own are promoted to MGL_WIRE_DECLINE_AT;
  the ones already under a W/E _ONCE line keep that line and gain the count.
- scripts/ci/wire_declines_audit.py: fails on a row with no site, a site
  naming no row, or a bare Count() with no MGLOG_W/E within 8 lines above
  it. WireDeclineTally.h (the macro's definition) is not a site. Wired into
  the job that runs the fatal census.

Audit before: 52 rows, 40 with sites, 10 unlogged under the W/E rule (rc 1). After: 53 rows,
53 with sites, 0 unlogged (rc 0). Forced red (ShapeNoVkFormat made true for
1x1 textures, reverted): Split.Fm.F1WireScenario.VertexIdSampler... fails on
pixels with ShapeNoVkFormat=2 / TexShapeSync=2 in the shutdown tally.
2026-09-22 20:24:00 -04:00
swung0x48 cc3c18c578 [scripts, docs] (P7): magma-b2.md counts the baseline as 73 + 11 + 2 + 2, carries the review round's §12 lines and gates, and the ratchet baseline names the tree that measured 173
ID-P7-31 review, note 3 and nits.

  * §0's preamble: the +4 over package B's 84/63/65 is not "package A landing after
    B" - A landed BEFORE B and is +2 (magma-a.md:151-153); package C is the other +2
    (magma-c.md:59-61). 73 + 11 (B) + 2 (A) + 2 (C) = 88/67/69.
  * §2.3 / §2.5: MsResolve1 is split+spawn only; the day-of +3 on tcp is marked as
    superseded.
  * §2.7 (the server-side knob has no tcp arm, measured) and §2.8 (flip mirrored,
    scale declined, X / cross-format mirror declined; three red-once lines).
  * §6: the lines for CONTRACT-P7 §12 - the scaled second leg is unclipped
    (VUID-vkCmdBlitImage-dstOffset-00248/00249, shared with the monolith);
    MagmaWireFatal("multisample-resolve-region") for a source rect outside the read
    buffer, where GL promises undefined values rather than an error; the 1->N
    identical-rect decline declines a legal shape (18.3.1 sample replication) because
    the monolith's own observable is a VU violation; the tcp lane cannot carry a
    server-side knob; X and cross-format Y mirrors of a depth/stencil resolve decline.
  * §7: the review round's gates - magma 94/73/70, full-split 525, unit 2423, parity
    RC 0 naming the five MAGMA_SERVER_ENV_KNOB_NO_TCP entries, census 79, ratchet 173,
    G1 .text 0xa52203 with 0/0 symbol drift, @P7 19 comment lines.
  * link_ratchet_baseline.txt: `base commit` points at 6af422b3, the tree that
    measured 173, not 4bee1313.

The "@P7 = 24 lines" nit needed no edit: the note already reads 19 at 6af422b3, which
is what the tree measures.
2026-09-22 20:05:28 -04:00
swung0x48 9fed478dfd [MG_IntegrationTest, scripts] (P7): a server-side magma test knob is registered on split and spawn only, and the parity gate names the five entries tcp cannot carry
ID-P7-31 review, should-fix 1.

Every MGITEST_MAGMA_FORCE_* knob is read BY THE SERVER (the getenv readers in
WireFramebuffer.inc run on the apply thread), and a ctest ENVIRONMENT property only
reaches the process ctest starts - the client. Inproc runs the server in that process
and ServerSpawn.cpp copies ::environ into the spawned child, so both arms see the knob;
the tcp arm's server is the lane-wide TcpServer.Start fixture, started once from
scripts/ci/tcp_server_fixture.py's own os.environ, and nothing a per-case property says
reaches it. The tcp copy of each knob=1 entry ran the DEFAULT arm under a name claiming
otherwise.

Measured, not deduced: a temporary fatal in each of the three readers killed all five
knob=1 entries on split and on spawn and left all five green on tcp.

So ShaderMip1, ShaderMip2, DepthMip, DefaultBlitShape1 and MsResolve1 are registered on
split+spawn only (the first four are package B's pre-existing vacuity of the same
shape). The knob=0 twins DefaultBlitShape0 / MsResolve0 keep their tcp entries: zero is
the default and means the same thing on every arm.

spawn_lane_parity.py names the exception, following ID-P7-14's MAGMA_INPROC_ONLY:
MAGMA_SERVER_ENV_KNOB_NO_TCP, applied through a new compare_arms(no_tcp=) that drops the
keys from the tcp comparison ONLY - folding them into inproc_only would stop requiring
them on spawn, which is where they do most of their work.
2026-09-22 20:05:28 -04:00
swung0x48 3c2867d34d [MG_Backend, scripts, docs] (P7): in a disaggregated build the monolith arm blits and mips with the baked modules, and the ratchet's p7-magma bucket falls to 88
CONTRACT-P7 §5.2's (B'), which §4.2 needs.

WHAT MOVES. In a disaggregated build the monolith arm now drives the SAME baked
modules the wire arm does:
  * InitializeBlitResources / InitializeDepthMipmapResources return true - there is
    nothing to build on any transport;
  * TryBlitToDefaultFramebufferWithShader turns its two BlitImageBindings into
    WireImages and calls BlitWireColorToDefault (WireColorBlit.inc);
  * GenerateDepthMipmapWithShader loops GenerateWireDepthMipLevel
    (WireDepthMipmap.inc) and hands the image back in the finalLayout its caller
    promised;
  * GetOrCreateBlitPipeline, the three GLSL strings, the eight kHidden* ids and the
    frontend members of BlitResources / DepthMipmapResources compile out.
The `#else` is the pull build's code, statement for statement.

WHY AN `#if` AND NOT A RUNTIME BRANCH - this is the whole of §4.2. P5f fv already
gave both initializers a non-monolith early return, and the ratchet counted the
symbols anyway, because a reference is a reference whether or not the branch runs.
And the MEMBER has to go with the construction: a SharedPtr member's destructor is
itself a reference to ~ProgramObject.

THE BUCKET TABLE (scripts/link_ratchet.py --bucket, build-split):

  bucket             before   after   a6
  p6-core                11      11    6
  p7-magma              101      88  101
  p3b-p4b-espryt         13      18   14
  both-backends          61      56   60
  total                 186     173  184

p3b-p4b-espryt's +5 is not a regression: those five MOVED out of both-backends
because DirectVulkan stopped referring to them and only DirectGLES still does.
Thirteen symbols went away outright, and they are exactly the three classes §4.2
names: ProgramObject::{AllocateLifetimeId, AttachShader, Link, ~ProgramObject},
SamplerObject::{SetLodRange, SetWrapR, SetWrapS, SetWrapT} and
ShaderObject::{Compile, DropCompileNode, JoinPendingCompile, ReleaseCompileNode,
SetShaderSource}. Re-baselined in this commit with --write-baseline; the 7 `# P13`
annotations carried over.

§4.2's "down to 0" DOES NOT HOLD, and the gap is not (B') left half-done - it is
that 88 of the 101 were never about the hidden programs. 62 come from
UniformManager.cpp.o and 14 more from VkSamplerManager.cpp.o: the MONOLITH
descriptor binder and sampler cache reading the application's own GL texture and
sampler objects (TextureObjectBase 35, TextureObjectWithOneMipmap 20 with its
vtable and typeinfo, SamplerObject's fourteen getters). Under a transport
SetupDraw branches to SetupWireDraw and not one of those lines runs - but that
branch is a RUNTIME one, which ID-P7-5 deliberately leaves to the optimisation
phase. notes/p7/magma-b2.md §4.3 names all eleven referring-object groups and what
each is.

NONE OF THEM IS ANNOTATED `# P13`, on purpose: that mark is for a symbol that
cannot fall without moving pull `.text`, and nothing above has shown that.
UniformManager's and VkSamplerManager's monolith arms are pull-build code, and
`#if`-ing them is the same operation this commit performed on VulkanRenderer.cpp.
They are wave 3's work, not P13's boundary problem, and marking them P13 early
would record an untried thing as a tried-and-impossible one.

Gates: integration-gpu 1705 (every monolith `DirectVulkan.` entry is in there),
integration-magma-split 93, -spawn 72, -tcp 74, integration-magma-full-split 524,
ctest -L unit 2423, all green; fatal_census 79 abort sites / 44 family words / 0
unmarked; spawn_lane_parity green; link_ratchet --assert-monotone unchanged at 173
against the re-baselined file, and --self-test OK.

RETRACE, which is this slice's load-bearing reading because the monolith arm
changed implementation: OpenRA.DirectVulkan (monolith) ssim 1.000000,
mismatchPixels 0, twice; OpenRA.DirectVulkan.SPLIT ssim 1.000000, 0. The whole
DirectVulkan matrix is 39/40 monolith and 38/39 split, and both reds are the SAME
case, minecraft-1.21.4-fabric-iris-iterationrp-in-world (ssim 0.027915), which is
PRE-EXISTING - it fails identically on ~/w7/p7-magma-b (this branch's parent),
because it needs CONTRACT-P7 §2.4's three iterationRP knobs and those are exported
by the CI job rather than by the ctest entry.

G1 pull .text 0xa52203, nm --defined-only identical to p7-before/pull-syms.txt
(30570 lines, 0 diffs).

Device debt: the monolith arm in the Android disaggregated package now runs the
baked blit, and its ROTATE_90/180/270 geometry has never executed - every surface
on this host is IDENTITY. notes/p7/magma-b2.md §5 lists it with the package's three
other device items.
2026-09-22 18:29:22 -04:00
swung0x48 394df89237 [scripts, ci] (P7): the memo purity gate asserts its own anchors and allow-list, and stops calling a policy exemption an absence
Rework round 3 of 4 after the D2 review (fable, ID-P7-8). Three defects, each of a kind
the gate could not have reported about itself.

(a) TWO STRUCTURAL SELF-CHECKS. family_spans() returned whatever matched and nothing ever
asserted that each of the five FAMILIES anchors found its declaration, so a renamed struct
would drop a whole family while every rule kept passing. And an allow-list entry no rule
can consult documents a carve-out that is not happening - a claim about the tree that
nothing verifies. main() now fails on either, naming the anchor or the entry and saying
which of the two repairs is wanted. Both are exercised: renaming an anchor and adding a
dead entry each red the gate.

(b) THE DOCSTRING AND THE test.yml COMMENT WERE WRONG about what the guards mean.
MOBILEGL_PIPE_LEGACY_MEMOS defaults ON (CMakeLists.txt:43) and no job here clears it, so
CI split builds DO compile those arms - TwinLookupMemo (DirectGLES.cpp:114) sits inside
one and is compiled into every lane this repo runs. The rule is "the legacy arm MAY KEEP
pointer keys", a policy exemption; only !MOBILEGL_BUILD_DISAGGREGATED is code a split
build genuinely never sees. Both now say so.

(c) FRONTEND HAD NO LEADING WORD BOUNDARY, so BackendSamplerObject* matched SamplerObject
and the gate reported the memos' backend VALUES as frontend keys - which the first version
answered with two allow-list entries, i.e. it documented a false positive instead of
fixing it. (?<!\w) added, with controls both ways: the backend twins must stay green and
the qualified MG_State::GLState::ProgramObject* must still match.

The allow list therefore goes from six entries to ONE. Four were dead on arrival - two
false positives now fixed at the source, one naming a typedef with no frontend type in it,
and const void* program / Uint64 programLifetimeId, which no rule could ever consult
because void is not in FRONTEND and neither line calls GetLifetimeId(). The
ResolvedTextureBindingMemo monolith key those last two described is still deliberate and
still there; it simply never needed an exemption, and saying it did was misleading. What
remains is the twin registry's UnorderedMap<StateObject*, Entry>, which really is
suppressed and really is the pull build's only arm.
2026-09-22 18:17:25 -04:00
swung0x48 6567bc1e16 [scripts, ci] (P7): a purity gate for Espryt's re-keyed memo families
P3b/P4b deepening, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, wave 2-D
package D2): "一条纯度 grep 门:split 构建下这些 memo 键里不得出现前端指针类型;接 CI".

scripts/ci/espryt_memo_purity.py, a sibling of link_seam_purity.py in the same style:
a hard-coded source list, a code_only() comment strip, (pattern, message) rules, and
a --self-test that runs the negative controls and then the real scan in one
invocation - which is what lets test.yml pass only --self-test and still get the
gate. Wired in next to link_seam_purity.py.

It scopes the rules to the BRACES of the families the plan names -
ResolvedTextureBindingMemo, SamplerPassMemo, UnitSamplerLookupMemo, TwinLookupMemo,
StateBackendObjectRegistry (the program registry and its five siblings) - plus the
image sweep, which is seven file statics rather than a struct and is matched by name.
Scoping matters: these two files pass frontend pointers around legitimately in
hundreds of places, and a file-wide grep would be a list of exceptions rather than a
gate.

Guard-awareness is a small conditional walker, not a preprocessor: it recognises
#if MOBILEGL_PIPE_LEGACY_MEMOS, #if !MOBILEGL_BUILD_DISAGGREGATED and the #else of
#if MOBILEGL_BUILD_DISAGGREGATED, which is every spelling these files use, and says
so in the docstring.

Six allow-list entries, each carrying its one-line reason. The two that matter:
StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry> and
ResolvedTextureBindingMemo's type-erased `const void* program` are the PULL build's
only arms - deleting them moves pull .text and fails G1's 0/0/0/0, so they retire
with the pull path (P13), not here. The other four are memo VALUES rather than keys.

Seven negative controls turn red; four positive controls stay green, including a
LEGACY_MEMOS arm, the #else of a DISAGGREGATED arm, the allow-listed type-erased key
and a frontend pointer outside every memo family. Verified once more by injecting a
fresh unguarded ProgramObject* field into the real ResolvedTextureBindingMemo: the
gate reddens and names it.
2026-09-22 17:31:51 -04:00
swung0x48 fdd611e83d [MG_Backend, MG_Test, MG_IntegrationTest, scripts, docs] (P7): the depth mip chain is baked, and every baked header in the tree now has to match its source
CONTRACT-P7 §5.1's (A) and (D), and (D) is what makes (A) safe to have.

(A) WireDepthMipmap.{vert,frag,inc}. VK_FORMAT_FEATURE_BLIT_DST is OPTIONAL for depth
formats, so a depth chain through a shader is the ordinary case and not the exotic
one - the monolith arm has carried such a fallback since before the split. The wire
arm had none, so a depth texture the device would not blit died on the first
disjunction of `mipmap-shader-format-or-shape`, and a DEPTH|STENCIL one died a guard
earlier as `depth-stencil-mipmap@P7` whether or not it could be blitted.

The monolith's fallback is not reusable, which is the whole of (A): it builds a GL
ProgramObject, two ShaderObjects and a GL sampler, binds through the uniform manager's
global UBO and writes uSrcTexelSize as a default-block uniform - four frontend objects
and a GL compiler on the server's apply thread. The baked program is the same shader
with the same arithmetic on a push constant and its own descriptor set. The fragment
stage is a PORT and not a rewrite: §3.2 retires a refusal by making the wire arm
observably the monolith's, so the 2x2 box lands where the monolith's lands, including
the half texel its ivec2(texCoord * srcTexelSize) sits off the destination centre.

`depth-stencil-mipmap@P7` therefore splits. DEPTH-only retires into the baked program.
DEPTH|STENCIL declines - MGLOG_E_ONCE and return, before the chain is grown, so a level
this verb will not fill is not created and then recorded as defined. A decline rather
than a Fatal because the monolith does no better: its fallback asserts depth-only, and
an assert is nothing in the release build both arms ship as. Nothing in GL promises a
stencil mip either, and this pass writes gl_FragDepth - it would leave the stencil
aspect of the level it just produced undefined.

(D) MOBILEGL_BAKED_INTERNAL_SHADERS. A baked header is a compiler output committed with
its input beside it and nothing connecting the two: edit the source and the build stays
green while the shader that runs is the one from before the edit. MG_Test's
BakedInternalShadersTest recompiles all eight arrays - WireColorBlit x2,
WireDepthMipmap x2, the iterationRP witness, the three probe stages - from their
committed GLSL with the in-tree glslang and compares word for word.

THE GATE IS THE BAKER. scripts/bake_internal_shaders.py compiles nothing; it runs this
binary with MOBILEGL_BAKE_INTERNAL_SHADERS and splices the words it emits. A gate that
used the glslang LIBRARY while the baker shelled out to a glslangValidator BINARY would
be two compilers with no rule about which is right, and the day their defaults diverge
the gate reds on a header nobody can regenerate.

What the first run measured, which is the one thing nobody could know beforehand:
WireColorBlitSpirv.h and all three arrays of PrimitivesGeneratedNoXfbProbeSpv.h were
already fresh - the hand-run glslangValidator words reproduce exactly. The iterationRP
witness was stale, as its own prose said; it is regenerated here (2142 -> 2143 words),
and the witness magic 0x50323033 is still there because it is a constant in the .comp
rather than a property of the words. All three provenance comments now point at the
script and the gate instead of at a tool on somebody's PATH.

Scenarios, on the three Magma arms: GenerateMipmapDepthWithoutNativeBlitPixels runs
DEPTH_COMPONENT24 and DEPTH_COMPONENT32F under MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1 and
reads level one back through a real sampled attachment, asserting per half and leaving
row 3 - the straddle band - unpinned, because pinning it would assert the half-texel
offset rather than the mip. GenerateMipmapDepthStencilDeclinesAndKeepsTheSession takes
no knob (the decline is on the aspect, before blit support is asked about) and pins that
the session survives and level zero still holds what was cleared into it.

Red once, three of them, executed and reverted:
 - a one-character edit to WireDepthMipmap.frag (no rebuild - the gate reads the source
   at runtime) reds the WireDepthMipmapFragment row at word 279;
 - with the depth arm disabled, the DepthMip entries die with Fatal{UnmigratedVerb,
   "Magma:mipmap-shader-format-or-shape format=125 ... aspect=0x2 ...
   missing=COLOR_ATTACHMENT"} - format 125 is X8_D24_UNORM_PACK32, which slice 1's
   diagnostic names for us;
 - with the Fatal restored, the decline case dies as
   Fatal{UnmigratedVerb, "Magma:depth-stencil-mipmap@P7"}.

Gates: integration-magma-split 77 (+2), -spawn 56 (+2), -tcp 58 (+2), 0 red;
ctest -L unit 2410 (+8, the gate's rows); informational integration-magma-full-split
513, 0 red, 59 skip (base 510/0/57); fatal_census green (79 abort sites, 43 words, 0
unmarked); link_ratchet --assert-monotone unchanged at 186, p7-magma 101.

G1 holds and it was not obvious: the witness header is compiled into the pull build
too, and its word count changed. Pull .text is still 0xa52203 and the defined-symbol
name list is identical to ~/w7/p7-before/pull-syms.txt line for line (30570) - the
change lands in .rodata and widens no instruction encoding.

VulkanRenderer.h is edited outside this package's file list: the new pass needs four
private declarations next to the colour blit's. Flagged for the integrator.
2026-09-22 16:37:54 -04:00
swung0x48 df17c416c0 [MobileGL, MG_Backend, MG_Remote, scripts] (P7): the split wire comes up before the first frontend object, and the caps mirror stops deciding a family's fate from a placeholder
F1. `MG_State::Init()` runs before `MG_Backend::Init()`, and the second is what
starts the session - so `GLContext`'s `TextureState` constructor materialised its
eleven default texture objects, each of them emitted `resource_create` through
`FamilyIsLive`, and `CapsMirror::ServerConsumes(0x400)` answered every one of them
from a generation-0 placeholder with `callMask=0`. All eleven records were dropped
and the whole event was one WARN line:

  MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
  family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
  caps generation 0

It is NOT a race. The line reproduces byte-for-byte on lavapipe and on the Adreno
830, because the two Init calls are two statements on one thread. And "the legacy
pull path runs for it" is false on the client under split: there is no pull path
on this side of the wire, so withholding is not a fallback, it is a drop that only
a later respecify's self-healing create ever undoes.

Two of the three options the brief offered do not apply, and both facts are worth
recording. (b) "extend the P6.5 fix to inproc" is already the tree: `FinishStartup`
step 7 is the one funnel every transport reaches and it will not return without a
real snapshot. (a) "a bounded wait in Consumes()" cannot work alone and would
deadlock: at `MG_State::Init()` no handshake is in flight, and the one that will
land is started later BY THE SAME THREAD.

So the fix is the ORDER, with the rule behind it as the gate:

  * `MobileGL::Initialize` calls `MG_Backend::InitSplitRolesBeforeState()` before
    `MG_State::Init()`. `Init()`'s split arm becomes one body, `InitSplitBackend()`,
    shared with the direct callers that still reach `Init()`; the monolith arm keeps
    its place after `MG_State::Init()`, because moving it would move pull `.text`.
  * `CapsMirror::ServerConsumes()` refuses to answer from a placeholder. It waits,
    bounded, when a bring-up is genuinely in flight on ANOTHER thread, and otherwise
    dies as `Fatal{CapsBeforeFirstSnapshot, "<family>"}` through `SessionFail` -
    a new family word, one row in `FatalFamilies.def`, no new abort site.
  * Two arms are exempt and say why in place: the server arm (`MGPipeServerArm()` -
    the apply thread and the spawn server, where the client's mirror is the wrong
    object rather than a late one), and a process whose own configuration never
    asked for a split transport (`MG_Config::SplitTransportRequestedByConfig`, set
    by `MG_ConfigLoader::InitTransport`, which is what tells a real run apart from a
    fixture that assigns `MG_Config::Transport` by hand).

The read accessors keep P5's ruling untouched: `LogBackendInfo` reads
`GetRendererInfo()` during `MG_Backend::Init()` and an abort there is a process that
cannot start. Only the DECISION changed. The warning was not made quieter and the
callMask was not defaulted to all-ones.

`MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` (server side, disaggregated builds, inert unless
set) holds back the FIRST CapsSnapshot. On spawn/tcp that is a genuinely late
snapshot in another process and it exercises the client's step-7 wait; on inproc it
only makes `Start` slower, which is itself the proof that the only window a client
can read a placeholder in is the one before `Start` is called.

G1: `.text` 0xa52203 and the 30570-symbol list identical to p7-before/pull-syms.txt.
Census: 79 abort sites unchanged, 44 family words (was 43). Link ratchet: 186,
unchanged.
2026-09-22 15:55:08 -04:00
swung0x48 801f8edaa6 [scripts] (P7): the parity self-test patches the seam the Magma tiers read through
Package D added a test that patches lane_cases and hands spawn_lane_parity.py a
placeholder build dir; package L then made the three Magma tiers read through a new
lane_names, so the placeholder reached a real ctest and the unit label went red on the
integrated tree. Patching lane_names to an empty set on every arm makes those tiers
report "nothing to compare" and leaves the assertion about lane_cases intact.
2026-09-22 14:43:51 -04:00
swung0x48 8bb6309a1c [scripts, CI, docs] (P7): the 184-symbol link-closure ratchet lands as a gate
CONTRACT-P6.md 12.1 has carried "a ratchet recomputes the 184 in CI and asserts it only
ever falls" as an obligation since P6. The roadmap handed it to P6.5; the P6.5 line never
received it, and scripts/ci/ has never had anything that computes this number. ID-P7-7
gives it to P7 wave 0. This is it.

scripts/link_ratchet.py mechanises a6's hand experiment: partition the per-object output
of the single MobileGL target into SERVER / FRONTEND / SHARED, then compute
(SERVER undefined) - (SERVER defined) - (SHARED defined) intersect (FRONTEND defined),
which is the undefined-symbol list a real link of a server-only image would print. The
partition is an explicit table with a rationale per row, because it IS the module boundary
the build system does not have (SOURCE_FILES is one flat list; boundaries are P13's work).
An object matching no row is a hard failure, as is a build whose CMakeCache lacks the three
disaggregated options, or an empty role set - every way this gate could go green without
having measured anything is an explicit exit 2.

Measured on 78b7d6be: 186 symbols. The three backend buckets reproduce a6's hand-made
101 / 14 / 60 exactly, as do its per-object referrer and definer distributions
(UniformManager 90, VulkanRenderer 60, Managers 53, DirectGLES 51; TextureObject 59,
SamplerObject 30, BufferObject 18, FramebufferObject 16, PipeFill 9). Nothing from a6's
184 has been cleared; the +2 are both P6.5's:
  Client::ClientSession::StartSpawned()                 - fork-per-session
  GLState::ProgramArtifactsSchemaFingerprint()          - archive v2 caps

The baseline stores the LIST, not a count (ID-P7-7): a count-only ratchet goes green on the
day one symbol is cleared and another added. --assert-monotone fails only on symbols absent
from the baseline and names each one's defining object and every referring object; symbols
that disappeared are reported with an instruction to re-baseline in the same commit, and
never fail.

The CI step goes in build-linux-split, not build-linux: build-linux takes the three options
from MOBILEGL_CI_DISAGGREGATED, which is ON only on feat/disaggregated and
codex/fix-github-ci, so on any other branch it builds a monolith and the gate would measure
a SERVER set with no transport half. The JSON is uploaded even when the gate fires.

Two findings for the integrator, both written up in the note:
  - a6's four published bucket totals sum to 181 against its own headline of 184. These
    buckets are a strict partition and always sum to the headline; the difference is in
    a6's core bucket, which it listed selectively.
  - 5 of the 14 "P3b/P4b" symbols are MG_Pipe slot-allocator and apply-thread plumbing
    that sits under MG_Impl/Pipe/ by file location alone. P3b/P4b cannot clear them by
    migrating Espryt functionality; they need P13's module move. That bucket's floor is
    5, not 0, so the plan's 0.5-day Tier 1 estimate for "D12's 14 symbols" should be
    re-cut against 9. All 7 symbols from that object carry a `# P13` annotation.

red-once (R-16) is recorded verbatim in the note. Worth knowing: the plan's own example
(SamplerObject::GetMinFilter) does NOT turn this gate red, because it is already in the
baseline - the ratchet reddens on new symbols, so a negative control has to pick one that
is not in the 186. A reach to GetBorderColorForm() from DirectGLES/Utils.cpp does, naming
the symbol, its definer and its referrer, exit 1; reverted and green again.
2026-09-22 14:26:04 -04:00
swung0x48 1de8f02ebe [MG_IntegrationTest, docs] (P7): the whole integration binary runs under Magma split, and it is one red
Tier 2 replays the split-arm registrations - ~100 scenarios, the set Espryt's disaggregation work
has carried since P5 - and it measured green on Magma across all three transports. That is a real
result but it is NOT exit gate 1. The gate says the INTEGRATION BINARY is green under Magma split,
and the set that names is the one the monolith DirectVulkan. arm runs: the whole suite minus
MGL_ITEST_MONOLITH_MECHANISM_FILTER. 510 cases against tier 2's 102, and the readback matrices,
the same-texture copy shapes and the odd-offset uniform ranges are in it and nowhere else in the
disaggregated registry.

integration-magma-full-{split,spawn,tcp}, in no gating step, private log per entry through a list
of its own. THE RESULT: 453/0/57 on inproc, 453/0/57 on spawn, 454/1/57 on tcp - and not one of
the 1530 entries reaches any of the tree's fifteen @P7 named refusals. The single red is
IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput on the TCP arm:
one texel of 262656, texel 0, which is the case's own exposure value
(IterationRPProgram203Scenario.cpp:371, actual half bits (0x357a, 0x4b65) against golden
(0x3a74, 0x4821)). Deterministic 5/5 on tcp; 5/5 GREEN on inproc, on spawn and on the monolith
control. spawn and tcp differ only in MOBILEGL_IPC_DATA, so the suspicion is the STREAM data path
and not "two processes"; the GLES tcp lane is green in the same run, so it is not the fixture.
Not fixed here - it is what the census is for.

TWO ENVIRONMENT DIFFERENCES FROM THE GATED TIERS, both measured rather than assumed:

  * NO MGITEST_SPLIT_LANE=1. That marker arms ScenarioFixture's destructor assertion - a case in
    an armed split lane must have moved the client encoder's record ordinal - which is right for
    a curated lane and wrong for a whole-binary census. With it set, the first run produced 13
    reds, all `EmitSeq was 20 at SetUp and is 20 now`: AdvertisedLimitsScenario (8),
    SpirvShaderBinaryScenario (4) and ProgramPipelineScenario.AReservedName - a limits query is
    answered from the client's CapsMirror and a shader-binary error surface never reaches a verb.
    Those 13 would red on a DirectGLES census too. What it gives up is stated in the comment: this
    tier does not prove its records crossed, and the tcp topology proof is inert in it.
  * DualBlockScenario.* filtered out. Its green REQUIRES MOBILEGL_IPC_ROLE_SPLIT_STATE=1 and a
    census must not set that knob; without it the case is red by name, which is its documented
    negative-control behaviour. The monolith arm skips it for the same class of reason.

CtWireScenario gets MOBILEGL_TEST_SERVER_COUNTERS=1 in this tier - a switch that makes a counter
readable, not an oracle that asserts - because without it the spawn arm's ApplierReset case reds
on "server control-record telemetry missing", which says nothing about Magma. The strict-arming,
pmap and persistent-map-arm markers stay out.

The inproc-only exclusion in spawn_lane_parity.py is now PER TIER: in the gated tier the two
scenarios are registered on the inproc arm alone, in the census they are on every arm and simply
skip, so excluding them there would manufacture a spurious "extra".

Measured after the change: every pre-existing lane unchanged and green (magma-split 73,
spawn 52, tcp 54, all-* 102/102/104, GLES 186/102/104, buffers 19, runahead 14, caches 7), the
dual-block census 257 green / 2 skipped / 0 red with an empty ratchet, both new CI steps green,
parity RC=0 across all three tiers, G14 4155 -> 6095 with removed=0.

The note now carries the grouped red list, the 57 named skips by reason, the four pieces of
evidence that the tcp red is a defect and not plumbing, and six findings - the first being that
wave 2's clusters cannot be scheduled from this lane's red list, because on lavapipe there is
essentially none.
2026-09-22 14:24:19 -04:00
swung0x48 3d9a38a979 [MG_IntegrationTest, CI] (P7): the split arms take a backend, so Magma has two-process lanes at last
P7 exit gate 1 asks for integration green "under Magma push and split". Until now it was not
merely unmet, it was UNMEASURABLE: every arm of mgl_itest_register_split_arms hardcoded
MOBILEGL_BACKEND_TYPE=DirectGLES (:2002/:2025/:2031) and MGL_ITEST_MAGMA_WIRE_ENVIRONMENT
hardcoded MOBILEGL_TRANSPORT=inproc (:2456), so the shared integration-split / -spawn / -tcp
labels held ZERO DirectVulkan entries and the 73 integration-magma-split cases only ever ran in
one process.

The registration macro now takes the backend as its first argument; the old name is a thin
wrapper, so all 32 call sites are untouched and every DirectGLES name, label and private log
path is byte-for-byte what it was. Two tiers come out of it, and the CMake comment states both:

  * GATED - integration-magma-{split,spawn,tcp}. The six hand-laid Magma lanes (Fm, Buffers,
    RunAhead, Caches, NamedBlit, P5fRsp) grow arms through one shared shape macro; the inproc
    arm keeps its names, its labels and its log paths, so the CI steps and the two magma_*
    checker scripts that name them by hand keep working unchanged.
  * INFORMATIONAL - integration-magma-all-{split,spawn,tcp}, in no gating step. Every scenario
    the split arms run, on Magma, on all three transports, generated by REPLAYING the macro's
    recorded call list - so a scenario added to the split arms tomorrow joins the Magma census
    the same day. Its job is to be able to go red while CI stays green; a case moves up into the
    gated tier with the commit that retires its blocker.

The label spellings are chosen against ctest -L's regex (ID-131 again): the tier word goes in
the MIDDLE, because integration-magma-spawn-all WOULD have been selected by
-L integration-magma-spawn and pulled ~100 expected-red entries into a gated lane. Sub-labels
mirror it: the inproc arm keeps integration-magma-buffers, the others put the arm word first.

MagmaRunAheadScenario and MagmaWireCacheScenario stay on the inproc arm alone, and that is
MEASURED rather than assumed: with all three arms registered, all 21 entries red on spawn and
tcp with no Fatal in either role log - the first arms its hold through
ServerLoopInstance().SetBeforeRetireHookForTesting, a hook on THIS process's server loop, and
the second asserts transportName == "inproc" in its SetUp. Both are named in the CMakeLists and
in spawn_lane_parity.py rather than allowed to widen a tolerance.

split_log_paths.py's is_split prefixes gain DirectVulkan.Spawn. / .Tcp. - on those arms two
processes append to one path, so a shared log does not merely blur attribution, it interleaves
two sessions. spawn_lane_parity.py gains the Magma dimension, normalising only the ARM segment:
RunAhead.Credit1. and .Credit3. are two entries of one case and a tail-stripping normaliser
would fold them together and then report an equal-sized set that had lost one.

Measured on this tree (lavapipe): magma-split 73 unchanged and green, magma-spawn 52/52,
magma-tcp 52/52, the three informational lanes 102 each, GLES split 186 / spawn 102 / tcp 104
all unchanged and green. G14 against ~/w7/p7-before: 4155 to 4565, removed 0, added 410.
2026-09-22 14:24:19 -04:00
swung0x48 484a56a5ae [scripts, MG_Pipe, MG_Backend, MG_Test] (P7): the census docstring's triage follows what the same wave retired, and the comments stop joining the @P7 census
Two corrections to the two commits before this one.

The widening's docstring was written against the 83 sites it found, and the commit that
routed five of them through Session::Fail did not revisit the prose. It now describes the
79-site floor the baseline actually records, says which five went and where, and states that
the enforceable artefact is the baseline's per-file map rather than the counts in the prose -
so a reader who finds the two disagreeing knows which one to believe. The three buckets are
restated too: `(ii)` is no longer "owned by a Ph / P7 row", because with the Magma wire arms
routed what is left there is the peer-reachable deaths in the applier and the two backends,
which is exactly the set ID-P7-1 scopes to Ph's fuzz arm 2 AFTER P7.

And the routing commit's own comments spelled the at-sign refusal marker while explaining
what routes through them, which put SEVEN comment lines into the retirement census: `grep
@P7` went from 15 sites in 4 files to 22 in 6, and that grep is how wave 2 will measure which
refusals it has retired. The prose says the word instead, and the count is 15/4 again.
2026-09-22 14:01:06 -04:00
swung0x48 a5dcbb444f [MG_Remote, scripts, MG_IntegrationTest] (P7): the supervisor names a faulted session, and the smoke that proves it is finally a ctest entry
Ph slice (1) (ID-P7-1). Three `::waitpid(active, nullptr, WNOHANG)` calls in the `--serve`
loop threw the status away, so a session child that died of SIGABRT inside SessionFail and
one that returned from its control loop and _exit(0)'d were the SAME EVENT to the
supervisor: `active = -1`, no line, nothing to count. ID-P7-1 declines the literal 98-site
latch translation because P6.5's fork-per-session already delivers "the next connection is
served as usual"; what was missing is that anyone could TELL a faulted session from a
finished one, which is what an operator, a fuzz arm and a flake triage all actually need.

One `ReapActiveSession()` where there were three call sites, asking WIFEXITED / WIFSIGNALED /
WTERMSIG - the same three questions ServerSpawn.cpp:289 asks of the same kind of child - and
logging `pid=<n> reaped exit=<c>` or `signal=<n>` with the supervisor's lifetime
`sessionsFaulted` beside it. A refused handshake exits 0 and is NOT a fault, which is right:
protocol.fbs:86 says a rejection is a normal session outcome. The running total is on every
reap line as well as on the summary, because the fixtures end a supervisor with SIGTERM and a
figure only printed at a chosen exit would never be seen.

THE IDENTIFIER ROW'S PREMISE IS WRONG, MEASURED. The plan asks for
CtrlEnvelopeBufferHasIdentifier at ServerMain.cpp:193 because ServerSession::ParseEnvelope
asks it and this loop did not - but the generated VerifyCtrlEnvelopeBuffer already IS
`VerifyBuffer<CtrlEnvelope>(CtrlEnvelopeIdentifier())` (protocol_generated.h:2378), so a
wrong identifier was refused here and ParseEnvelope's second call is redundant too. Recorded
for the integrator. What was real is that both refusals were SILENT: a peer whose
post-Welcome frame was garbage had its session ended with no line in the log, and a fuzz arm
that cannot tell "refused, by name" from "the child went away" measures nothing. So the
identifier is now asked first - it is the specific diagnosis where the verifier's is the
general one - with the length guard that asking it first requires, and both say so.

scripts/ci/tcp_supervisor_smoke.py has been in the tree since P6.5 asserting four supervisor
controls and was referenced by NOTHING - no ctest entry, no workflow step - so nobody could
have said whether those four were green. It is now `TcpLane.SupervisorProtocolControls`
beside the TCP lane's own fixtures, labelled integration-tcp, under the lane's resource lock
(a second supervisor's child would answer the other's peers with Busy). It needs flatc, which
is deliberately not in the build graph, so it resolves one gen_protocol.py's way and exits 77
- ctest SKIP - when there is none, rather than passing on a run that did not happen.

Two cases added to it: a session child killed with SIGABRT must be named by pid and
`signal=6` with `sessionsFaulted` one higher than before, and the next Hello must still be
welcomed; and a steady-state frame carrying the wrong file identifier must be named rather
than silently dropped.

Red-once, both ways, against the real image: making the reap discard the status again reds
with "the supervisor did not name session pid=N as killed by signal=6", and deleting the
identifier branch reds with "read a frame with the wrong file identifier without saying so".
2026-09-22 14:01:06 -04:00
swung0x48 e1ec69341a [MG_Pipe, MG_Backend, MG_Remote, MG_Test, scripts] (P7): the Magma wire fatals and the two Staged aborts go through Session::Fail
Five deaths in the split server image raised their own std::abort(). Three of them are
funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), WireDescriptorFatal
(Renderer/UniformManager.cpp) and WireBufferLegacyFatal (Renderer/WireDraw.inc) - and the
fifteen `@P7` refusals on the tree (8 in UniformManager.cpp, 4 in WireFramebuffer.inc, 2 in
WireDraw.inc, 1 in VulkanRenderer.cpp; plan section 1.3's count confirmed) all die through
two of them. Every one of those deaths published NO SessionFault frame, so the peer read a
bare EOF; bumped NO SessionFaultCount(), so exit gate S8 could neither confirm nor falsify
it; and carried a hand-typed family word with no path to FatalFamilies.def's projection.

MG_Backend may not name an MG_Remote symbol on its own account, so the route is a seam in
MG_Pipe with MGP_TRIP_WIRE_REPORT's shape (PipeApply.cpp:66, and the new code sits beside
it): MG_Pipe/PipeSessionFail.h declares MGPipeSessionFail, PipeApply.cpp defines it inside
`#if MOBILEGL_BUILD_DISAGGREGATED`, the DEFAULT behaviour is byte-identical to what the
three funnels did on their own, and MG_Remote::InstallPipeSessionFailHook() points it at
SessionFail. The install is in InitServerRoleCommon (MG_Backend/Init.cpp), step 0, because
that is the one function BOTH server roles run - the inproc role and the spawn/TCP session
child - so there is no per-transport install to get wrong.

MGPipeFatalFamily is a two-value boundary enum, NOT MGFatalFamily cast to an integer: the
.def's enumerators are positional and a row inserted in its middle would silently re-point
every hook call at a different family. FatalFunnel.cpp's adapter is the one place the two
vocabularies meet, as a switch with a named default.

The family word stays UnmigratedVerb rather than becoming a new MagmaWireRefusal. Three
reasons: the message strings are required to stay byte-identical (5.2), and the retrace
refusal censuses recorded since P5b count these deaths by that word - a rename invalidates
every `@P7` measurement taken against them; `Magma:` in the detail already separates the
Magma arm from the GLES UnmigratedVerb deaths, so a new word adds no discrimination a grep
does not have; and its wire projection would be ProtocolCorruption, exactly UnmigratedVerb's.
Recorded for the integrator.

Ph slice (2) in the same commit: Server/StagedShadow.h's RequireCovered and
Server/StagedTextureStore.h's RequireLevelBytes were MGLOG_F + bare abort. Both are inside
MG_Remote already, so they call SessionFail directly, message verbatim;
check_include_closure.py's four probes do not reach either header and the gate is green.

Census: 83 abort sites -> 79 over 20 files (five sites retired, one gained for the seam's
no-hook default, which is argued into FUNNEL_SITES), baseline rewritten in this commit - the
downward ratchet's notice is what asked for it. Two gate bugs the widening exposed and this
commit fixes: the SessionFail call regex also matched `MGPipeSessionFail(` (a lookbehind
now), and a funnel's anchor needed a wider window than a marker's (kFunnelAnchorWindow).

Red-once, three ways. Pointing MagmaWireFatal back at its own abort reds the census naming
WireFramebuffer.inc 0 -> 1. Making the seam skip the hook reds both new death cases -
`TheMagmaWireVerbDeathReachesSessionFailAndBumpsTheFaultCount` on the exit code, because it
reads SessionFaultCount() out of a SIGABRT handler and gets 0 instead of 1, and
`TheSeamHandsTheInstalledHookItsFamilyAndItsLineVerbatim` on the line the hook echoed.
2026-09-22 14:01:06 -04:00
swung0x48 f141918aa1 [scripts, MG_Remote, MG_Impl, MG_Test] (P7): the fatal census gate widened to the split server image
`scripts/ci/fatal_census.py` scanned `MG_Remote/` and saw four abort sites. CONTRACT-P6
§12.1 already says what the server image is - "P6's server image is the whole
libMobileGL.so" - and the server process was executing seventy-nine more deaths the gate
could not see: 15 in DirectGLES.cpp, 12 in Managers.cpp, 11 in VkBufferManager.cpp, 7 in
PipeRoute.cpp. SCAN_ROOTS is now MG_Remote / MG_Pipe / MG_Backend / MG_Impl/Pipe /
MG_State, `.inc` joins `.cpp`/`.h` (the Magma wire arms and the generated wire records are
.inc files, and 20 sites are reachable only through one), and MG_Benchmark /
MG_IntegrationTest join MG_Test in the exclusion.

Four rules now, where there were two:

  1. an abort names its family within twelve lines, OR it is argued into FUNNEL_FILES /
     the new FUNNEL_SITES table. FUNNEL_SITES exempts an individual funnel inside a file
     that has ordinary named deaths around it, and only when one of the file's anchors
     appears in the same window - so the exemption follows the funnel if it moves, expires
     if it is deleted, and does not cover an unrelated abort added to the same file. Two
     entries, one line of reason each: PipeApply.cpp's MGP_TRIP_WIRE_REPORT and
     PipeWire.inc's MGPipeWireProtocolFatal.
  2. unchanged: a WireLogFatal / SessionFail call carries a `Fatal{` in its string.
  3. NEW: every family word found in the perimeter has a row in FatalFamilies.def. Eleven
     words did not, and now do (see below).
  4. NEW: every `Refuse{Word}` names a RefuseCode enumerator. This is what catches
     `Refuse{AuthenticationRequired}` (SocketTransport.cpp:161), a word protocol.fbs has
     never had - PH-7 (2). Renamed to the enum's own `Authentication`, and
     SocketTransportTest pins the spelling to `EnumNameRefuseCode` so the two cannot
     drift again.

`abort_sites` was recorded and never compared. It is now a DOWNWARD ratchet with a
per-file map beside it, so "83 became 84" reports as "VkBufferManager.cpp went 11 -> 12,
here are its lines"; fewer sites - the direction `@P7` retirement moves the number - asks
for a re-baseline rather than blocking one.

Eleven new `.def` rows for the eleven rowless words. Ten are ProtocolCorruption for the
reason that bucket already states. ResourceUnavailable is ServerCrashed, not SegmentMismatch:
SegmentMismatch means a shared SEGMENT was the wrong size, and telling a peer that when a
VkBuffer allocation failed would be a lie about whose geometry was wrong. A twelfth word was
collapsed rather than given a row - `InitialBytesNotCarried` (PipeFill.cpp) said the same
thing as `UncarriedInitialBytes` (WireTables.cpp) about the same record, and two words for
one family is the drift a6 censused.

Red-once, four ways: an unnamed `std::abort()` appended under MG_State reds naming the file
and the line; deleting the `Fatal{` from PipeRoute.cpp's ResidualBlockSize reds; putting
`Refuse{AuthenticationRequired}` back reds rule 4; a typo'd family word reds rule 3 and the
vocabulary ratchet together.
2026-09-22 14:01:06 -04:00
swung0x48 3ba150d021 [tools, android-plugin, CI] (P7): the device matrix runner selects DirectVulkan, repeats are archived, and the spawn env parse is entry-aware
wave 0 item 5. Three instruments P7's device window needs, and the CI steps that
can make each of them red.

run_tcp_matrix.py --backend DirectVulkan end to end. --backend already existed and
already filtered by each case's ci_backends; what did not exist is a per-backend
GOLDEN. The two backends shared `golden` plus an optional `alternate_golden`, and
alternate_golden is an OR across backends, not a per-backend key - so a
DirectVulkan run that matched the DirectGLES picture passed, which is the "went
green on the wrong arm" shape the exit gate exists to catch. trace_cases.json
gains `backend_overrides.<backend>` ({golden, alternate_golden, ssim_threshold}
only: restating target_call or a crop would stop the two backends retracing the
same frame through the same window). A per-backend golden REPLACES the shared
pair, matching another backend's declared image is refused BY NAME, and a --case
named explicitly that does not run a requested backend is an error instead of one
plan fewer and no message. The CMake emitter and the APK matrix refuse a case that
declares the key rather than silently emitting one golden for both arms.

DirectGLES is unchanged and that is asserted, not asserted-in-prose: a case with no
override resolves to itself, and the key is dropped from the resolved case even for
the backend that overrides nothing - so giving DirectVulkan its own golden does not
move the DirectGLES identity fingerprint and does not invalidate its checkpoints.
Each results.json row now records the golden, alternate golden and threshold it was
scored against.

run_android_retrace_local.py gets --matrix (the CI split subset, 39 cases, each
running only its own ci_backends - not --all, which adds the rd12 workload ID-P7-4
excludes), --repeat N, and --archive-dir. The archive is the point: the result root
is keyed by case and backend and by NOTHING else, so repeat 2 overwrites repeat 1
and the only artefact that survives a three-repeat run is the last one, which makes
"were the three repeats the same picture?" unanswerable after the fact - in the one
place where re-running to find out is expensive. It also refuses --benchmark with a
non-monolith --transport, which has always produced a monolith run under a spawn
label (run_benchmark_case passes args.env alone; p6/README.md records the pitfall).

compare_actuals.py computes the pair result.json cannot: SSIM between two actual
PNGs, plus a case x repeat x ssim-vs-golden x ssim-vs-first-repeat table over an
archive. It is the retrace gate's OWN SSIM - a transcription of ComputeChannelSsim
/ ComputeRgbSsim (trace_replay_core.cpp:630/:671), the only SSIM in the tree - and
the crop comes from each repeat's own result.json, so the two columns describe the
same rectangle. PNG decode is stdlib zlib, because an unavailable Pillow in the
device window would turn "the repeats diverged" into "the tool did not run".

TraceReplayActivity: resolveSpawnServerPath's two decisions were String.contains()
on the whole joined K=V;K=V list and neither was entry-aware. The one that bit:
`--env MOBILEGL_IPC_SERVER_PATH` with no '=' is the documented UNSET spelling
(trace_env_overrides_test.cpp:92) and the only way to ask for "spawn with no image"
and watch LaunchServer refuse by name - it did not match "MOBILEGL_IPC_SERVER_PATH="
so a resolved path was appended AFTER it and, because ApplyEnvOverrides applies in
order with setenv(...,1), the deliberate unset was silently undone. That is exactly
the "silently replacing it would make that knob untestable" the method's own comment
forbids. Two more: the key inside another entry's VALUE flipped both decisions the
wrong way, and the transport match was case-sensitive while ConfigLoader lower-cases
the value before matching (ConfigLoader.cpp:322). The parse moves to
SpawnServerPath, a class with no android.* import, so test_android_lifecycle.py
compiles and runs it with plain javac - the parse had no host-side coverage at all,
and its on-device failure mode is "the knob had no effect".

Only spawn gets a path, deliberately: it is the only transport that execs an image.

Red-once, all four: a stub case whose result names the DirectGLES golden fails a
--backend DirectVulkan run with "matched a golden another backend declares"; a
synthetic PNG pair with a closed-form SSIM (identical = 1.0 exactly, constant
black vs white = C1/(255^2+C1), inverted halves = (C2-2v)/(2v+C2)); the pre-fix
resolveSpawnServerPath body fails eight of SpawnServerPathTest's checks; and the
DirectVulkan TCP proof battery is the DirectGLES one under the other prefix,
because an empty prefix selection raises nothing and reads as a clean pass.

CI: test_run_tcp_matrix.py and test_compare_actuals.py join the R-16 step. The
matrix driver's stub regressions had NO CI step - they were run by hand and
recorded in a note - and they guard the tool that decides whether a device golden
run passed.

Counts in WSL: test_run_tcp_matrix 12 -> 18, tcp_lane_tools 4 -> 6,
test_compare_actuals 12 new, test_android_lifecycle green with the new Java unit.
2026-09-22 13:58:07 -04:00
swung0x48 fe28bdb5f3 [MG_Remote, MG_Pipe, MG_Test, CI, tools, docs] (P6.5): the first wave completed and reviewed - archive v2, publication boundary, half-close, AcceptPair/role fixes, device re-validation 2026-09-22 10:56:19 -04:00
swung0x48 018fab0f5b [WIP] (MG_Remote): implement P6.5 TCP and stream transport 2026-09-22 07:19:18 -04:00
swung0x48 242fd45cd7 [Fix] (CI): read role logs and retry device disconnects 2026-09-22 07:19:17 -04:00
swung0x48 9043442454 [MG_Test, CI] (P6): the last two readers the role split left behind
build-linux-verify could not compile PipeStatsTest: RoleDerivedJsonDumpPathForTesting is DISAGGREGATED-only and the verify flavour is deliberately monolith. The case name stays registered and GTEST_SKIPs there, matching the PIPE_PUSH pattern, so ctest -N still matches between flavours.

runtime_mode_proof.py logs opened MOBILEGL_LOG_FILE_PATH as a file. After the split that name is a base and the sink writes <base>.client.log / <base>.server.log, so the inproc proof died on FileNotFoundError after a green lane. It now reads through split_log_paths' role helpers and names the paths it looked for when none exist.
2026-09-22 04:40:03 -04:00
swung0x48 94c130d639 [MG_Util, MG_IntegrationTest, CI] (P6): the readers the role split left behind
The per-role log split renamed BOTH halves so that an un-updated reader would
fail loudly instead of silently reading half a session. It worked: CI found
four readers this way. This is them.

- MG_Util/Debug/Log.h: the role vocabulary now exists in the PULL build too,
  header-only. It was wholly inside #if MOBILEGL_BUILD_DISAGGREGATED, so
  PipeInputsTest - which builds in the verify flavour - could not name
  ReadRoleLogs and took the whole build-linux-verify job down at compile time.
  Inline rather than out-of-line because of G1: nothing in the library calls
  these, so an inline definition emits code into the readers and NOTHING into
  libMobileGL.so. Verified: pull symbols 30570 -> 30570, 0 added / 0 removed,
  .text still 0xa52203, and zero role-log symbols in the pull library.

- PipeStatsWindow.h: the harness copy of the naming rule is now flavour-aware.
  It derived `.client` in BOTH flavours, so in the verify build - one role,
  MOBILEGL_LOG_FILE_PATH written unchanged - it opened a file that never
  existed. The copy has to stay a copy: MG_Util's RoleLogPath is `t` (local)
  in the shipping .so this module links, so calling it does not link.
  Added LogMark/MarkLaneLog/ReadLaneLogSince: one offset PER ROLE, because a
  single scalar over the concatenation slides by whatever the other role wrote.

- The four arming scenarios: they asserted on lines the BACKEND emits, and
  under inproc the backend runs on the apply thread - the server role - so the
  line lands in the server's log. Reading only the client's half reported the
  emulation unarmed, i.e. accused the product of a defect the reader invented.
  Four copies of the same private helper pair now delegate to the harness.

- scripts/ci/magma_runahead_checks.py: it unlinked and read the base name as
  if it were a file, so the negative arm saw an empty log and could not prove
  the knob took. It now imports split_log_paths' derivation - the authoritative
  one - rather than becoming a third copy.

Local verification (WSL, lk-split + c6-pull):
  - the 4 CI-red entries: 10/10 PASS under inproc, 10/10 under monolith
  - unit role-log readers: 510/510 PASS
  - magma_runahead_checks negative: run_ahead_0 EXPECTED_RED, restored GREEN
  - the 5 affected TUs compile clean with MOBILEGL_BUILD_DISAGGREGATED=0
  - G1: 0/0/0/0, .text 0xa52203

Red-once is the CI run itself (35677991453), which failed for each of these
reasons before the change.
2026-09-22 00:37:39 -04:00
swung0x48 4bb4d4fa4f [MG_Remote] (P6/dl): every session death goes through one funnel
Step two of CONTRACT-P6 5.2. The ~90 scattered `MGLOG_F("...Fatal{...}"); std::abort();`
pairs under MG_Remote/ become one call each to SessionFail(MGFatalFamily, fmt, ...),
which logs, echoes to stderr, counts, and aborts in one place. a6's census
found no single chokepoint existed; now there is one, and step three hangs the
SessionFault-to-peer publish off it.

THE MESSAGE STRING IS PASSED VERBATIM. The contract requires the log lines, the
CI greps and every red-once byte-identical, so SessionFail does not compose the
line from the family - it takes the SAME string the site handed MGLOG_F and
forwards it unchanged. The family enum rides alongside, redundantly, so the
funnel has the coarse wire FatalCode without parsing the string. The census now
checks that redundancy the way it checks WireLogFatal: every SessionFail call's
string must carry a Fatal{ word, so the enum and the string cannot silently
disagree.

The conversion was mechanical - 89 sites by a transform that injects the family
as MGLOG_F's new first argument and drops the trailing abort - with three sites
fixed by hand that the transform would have broken: a stub macro, and two
if/else-then-shared-abort blocks (PersistentMapTracker's adopt-tier and
SurfaceOpCodec's three surface-error branches) where a naive rewrite stole the
abort from the earlier branches and would have let them fall through. The
death tests are the safety net for byte-identity, and every one stayed green.

Four std::abort() remain under MG_Remote/, all inside the two funnels
(WireLog.cpp, FatalFunnel.cpp); the census exempts both files because their
callers carry the family word, and its baseline drops from 93 aborts to 4.
SessionFaultCount() is the one telemetry point S8 will assert zero over a good
run.

G1 re-measured: 0 symbol diff, .text unchanged at 0xa52203 (FatalFunnel and
FatalFamily live under MG_Remote/, compiled only in the disaggregated build).
Unit 2358 (one pre-existing environmental failure), integration-split 186/186,
integration-spawn 102/102, retrace 6/6, census 0 unmarked.
2026-09-21 21:44:17 -04:00
swung0x48 5fb54f03fe [MG_Util, MG_Test, MG_IntegrationTest, CI] (P6): one log per role, and every reader moved with it
The spawn work had the two roles append to one MOBILEGL_LOG_FILE_PATH under
O_APPEND. It was correct - two processes at one offset would corrupt the file,
O_APPEND made each write atomic - but it put two sessions' lines in one place
and made every per-side assertion a search. This splits the sink: the client
role writes <base>.client.log and the server role writes <base>.server.log.

THE BASE NAME NAMES NO FILE ANY MORE, AND THAT IS THE POINT. Leaving the
client at the old unsuffixed path would have been the compatible choice and the
wrong one: a reader that was not updated would go on finding a file, go on
passing, and go on seeing only half the session - and a refusal census that
reports a confident zero for the half it cannot see is worse than one that
fails. With both names moved, an un-updated reader gets "file not found" and
says so. There were 34 such readers; every one is moved here.

THE ROLE IS PER-THREAD, not per-process, because under inproc both roles are
threads of one process. Log.cpp keys the sink on a thread-local role: the apply
thread declares itself server (ServerLoop.cpp), a spawned image is server for
its whole life (MOBILEGL_IPC_ROLE=server, read once), and everything else is
the client. The naming rule (RoleLogPath) and the two whole-log operations
(ReadRoleLogs / TruncateRoleLogs) are EXPORTED from the library, so no reader
reimplements the derivation - a second copy is the copy that falls behind.

Readers, by how they read:
 - unit mains set MOBILEGL_LOG_FILE_PATH and read a role file: they keep the
   base in the env and derive the client path from the library helper.
 - the death-test child runners (ReadLog) now concatenate BOTH roles: a death
   test asserts the CHILD said something, and the applier's refusals - the very
   diagnostics these check - are written under the server role.
 - the offset-based Emit readers derive the client path once after setenv.
 - PipeStatsWindow reads both, because draws/vbs increment in the backend's
   PrepareForDraw - the apply thread, the server role - so the summary line is
   in the server file even under inproc.
 - run_trace_case.cmake's split census reads both files; the client keeps the
   transport markers.
 - split_log_paths.py (and its self-test, and the control stub) derive both
   role files from the declared base.
 - the Android script pulls both role files off the device and concatenates
   them, so every downstream check is unchanged - and it judges a role file by
   CONTENT, because `run-as test -f` through `exec-out` returned 0 for a missing
   file on the device and `exec-out` folds cat's own not-found line into stdout.
   The monolith arm legitimately has no server file, and that is a silent skip
   rather than a one-line server log.

Guarded so a pull build is untouched: RoleLogPath and the role sink are wholly
inside MOBILEGL_BUILD_DISAGGREGATED; a non-disaggregated build writes the base
path exactly as before. G1 re-measured - 0 symbol diff, .text unchanged at
0xa52203.

Verified on device 2f7cbe2e: monolith writes only the client log (no phantom
server log), inproc and spawn each write both and pass their transport proofs
(spawn: server role in pid 16125). Unit 2353 (one pre-existing environmental
failure), integration-split 186/186, integration-spawn 102/102, retrace 6/6,
control smoke 12/12, split-log-paths self-test 9/9.
2026-09-21 21:26:05 -04:00
swung0x48 15f5b54bbd [MG_IntegrationTest, CI] (P6/t6): integration-spawn, and the set identity made structural instead of maintained
Exit gates 9.2 and 9.3. Until now nothing ran the integration suite across a
process boundary at all - the spawn shape was proven only by the retrace lane,
which exercises one trace per case and no scenario assertions.

integration-spawn: 102 cases, 102 green.

THE SET IDENTITY IS THE POINT, AND IT IS NOT MAINTAINED BY HAND. 9.2 asks that
the spawn lane's case-name set equal the split lane's. Registering the spawn
arm as a second copy of each block would satisfy that today and drift the first
time somebody adds a scenario to one of them - and a spawn lane covering LESS
goes green while proving less, with nothing about a smaller green lane looking
wrong. The eleven split-environment blocks become eleven calls to one macro
that emits BOTH arms from one filter, so a case added to either is added to
both or to neither.

scripts/ci/spawn_lane_parity.py asserts it anyway, because a macro is one edit
from a hand-written block. It compares SETS rather than counts - a count can
agree while the membership does not - and carries exactly one sanctioned
asymmetry, named in the file: integration-split also labels the PUSH-MONOLITH
arm, whose scenario is monolith-only by construction. R-16: introduce a
split-only block and it reds naming the case and the macro to use.

THREE CASES SKIP UNDER SPAWN, BY NAME. CtWireScenario reads PipeApplier's
in-process tallies through ServerVerbs(); under spawn those live in the SERVER's
process and this one's copy is 0 forever, so the assertions were failing about
somebody else's memory. That is a harness limit, not a product defect - the
records themselves cross, which the retrace lane proves. They are SKIPPED and
not deregistered: a case dropped from the registration would make the two sets
differ silently, which is the drift the macro exists to prevent. What retires
the skip is cross-process telemetry, which is also what S8's SessionFaultCount
needs.

TWO REAL REGRESSIONS THE EXISTING GATES CAUGHT, both from this refactor:

  * the macro dropped the per-block TEST_LIST variables, which were what
    SplitLogPaths fed to give every entry a PRIVATE MOBILEGL_LOG_FILE_PATH.
    SplitLogPaths.PrivateAndDistinct went red naming the first orphan. The
    macro accumulates list NAMES now, so a block added later is covered without
    anyone remembering to extend a list - and the names have to be unique,
    because gtest_discover_tests' TEST_LIST is populated when CTEST runs, not
    at configure time, so one shared variable held only the last block's
    entries. That matters more on the spawn arm: there TWO PROCESSES append to
    the path, so a shared file interleaves two sessions rather than merely
    being ambiguous.

  * the configure_file that substitutes those names ran BEFORE the F1 and Ct
    blocks registered, so it captured neither. Same gate, naming
    F1WireScenario.ClearBufferfiPixels. It now runs last.

junit_tally gains --require-spawn-ran beside --require-split-ran: the prefix IS
the arm, and a shared flag would let one lane tally the other's entries and call
itself run. Verified to discriminate - the spawn junit reads 96/0/5 under its
own flag and 0/0/0 under the split flag, which correctly fails.

Two death tests follow the ADOPT_TIER message the census commit retagged with
Fatal{UnimplementedAdoptTier}; their regexes asserted the old wording.

Unit 2353 (one pre-existing environmental failure), integration-split 186/186,
integration-spawn 102/102, retrace 6/6, fatal census 0 unmarked, G1 0 symbols
and .text unchanged at 0xa52203.
2026-09-21 20:20:24 -04:00