Commit Graph
39 Commits
Author SHA1 Message Date
swung0x48andClaude Opus 4.8 5968863fb9 [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.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-21 21:44:17 -04:00
swung0x48andClaude Opus 5 743d1facba [MG_Pipe, MG_Backend, MG_Impl] (P6/sm, P6/st): four guards that were compiled in and could never arm, and the crash behind one of them
CONTRACT-P6 3.2 (D1c), 3.3 (D13) and 3.4 (D10).

Every guard below asked a question a SERVER PROCESS answers wrongly - "is a
ClientSession active" (never, there) or "am I on the apply thread" (every
thread is, there) - so all four were present, compiled, and permanently
disarmed in the one shape they exist for. That is worse than absent: it reads
as coverage.

D13 FIRST, THEN THE WIDENING. MGPipeRoleSplitActive() meant "the REHEARSAL
knob is on AND the transport is real", so widening it in place would have left
a narrow name describing a wide predicate. It is renamed to
MGPipeRoleSplitRehearsalActive() across its 12 sites - including the
DualBlockScenario assertion message that embeds it - and the wider predicate
is introduced BESIDE it.

MGPipeBlocksAreDistinct() IS TRUE FOR TWO DIFFERENT REASONS, which is the
whole point: under the rehearsal the two blocks are distinct objects in one
process; under SPAWN they are distinct because they are in different ADDRESS
SPACES, and no knob is involved. The spawn retrace runs with
role-split-state=0 - measured off the lane's own log - so every guard gated on
the rehearsal was off there.

THE CRASH. MGPipeServerBlockNoteIdentity early-returned on the rehearsal, so a
spawn server's gPipeInputs.ContextIdentity() stayed nullptr for the life of the
process. DirectGLES' framebuffer binding-slot memo compares that identity FIRST
and with NO generation, against a cache variable that also starts nullptr - so
nullptr == nullptr reads as a CACHE HIT and the function returns a slot pointer
nothing ever filled. `sm` widens the gate; `st` lands the named Fatal
PipeInputs.h had already asked for, so a reopening says which memo and which
identity instead of corrupting memory three frames later. Guarded on
MOBILEGL_PIPE_PUSH: in a pull build that identity is the live GLContext's
address and is never null, so the test would be dead code and G1 measures that.

DISJUNCTS, NOT SUBSTITUTIONS - and this was measured the hard way. The first
attempt REPLACED OnApplyThread() with MGPipeServerArm() and
ClientSession::Active() with MGPipeSessionLive(). 14 RemoteGuards and
RemoteClientControls cases went green by omission: a ServerLoop fixture with no
client session has only the original facts, so swapping them out disarmed every
test that drives the apply thread directly. The originals stay; the new
predicates are added beside them.

AND THE PROBE HAD TO BE THE RIGHT ONE. Installing
ClientSession::ApplyThreadIsInsideApplier as the apply-thread probe made every
GL-thread call a role violation whenever the applier happened to be busy - the
inproc retrace died on Fatal{RoleViolation, "MGPipeSlots"} raised from the GL
thread. "Am I the apply thread" and "is the apply thread inside the applier"
are different questions; the second is R-1's mutual-exclusion probe and is
about another thread entirely. ServerLoop::OnApplyThread is the one the guards
mean.

Three of D1c's six predicates land here, the three the four guards need. The
other three (MGPipeSplitActive, MGPipeShouldDial, MGPipeBackendIsLocal) have no
D10 site and are not added as dead code; their call sites are ContextEpoch and
ActiveXfbState, which this commit does not touch.

D14 holds: the non-disaggregated build gets constexpr false definitions, not
runtime-false predicates, so every consult folds at compile time. G1
re-measured - 0 symbol diff, .text unchanged at 0xa52203.

Two test names added, both falsifiable by construction: the spawn case asserts
the blocks are distinct with the rehearsal knob OFF, and the monolith case
asserts the predicate is not simply a constant. Unit suite 2353 tests, one
pre-existing environmental failure. All six OpenRA arms green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 19:38:19 -04:00
swung0x48andClaude Opus 5 570ebde9bc [MG_Remote] (P6/hs): the build stamp that was silently empty, and two asymmetries the handshake never checked
CONTRACT-P6 4.2 and 4.3. Three defects, all of them things the handshake
believed it was checking and was not.

1. THE BUILD STAMP WAS "" AND NOTHING SAID SO. CMakeLists took the git hash
   through an execute_process with NO RESULT_VARIABLE, so a failed
   `git rev-parse` yielded the empty string. Verified live rather than
   inferred: this worktree's `.git` is a file holding a WINDOWS path, WSL's
   git answers `fatal: not a git repository`, result 128 - and every WSL build
   mixed "" into its ABI fingerprint. Two builds from different commits then
   agreed exactly, which is the one thing the stamp exists to prevent. The
   already-failing SessionHandshakeTest was reporting this the whole time.

   The fix may NOT touch GIT_COMMIT_HASH_SHORT: it is baked into
   glGetString(GL_VERSION), so redefining it changes an application-visible GL
   string and the pull build's .rodata - a behaviour leak and a G1 break at
   once. MGGitHash.h.in gains a SECOND pair instead, consumed only by the
   fingerprint, plus -DMOBILEGL_BUILD_STAMP for trees git cannot read (CI has
   ${GITHUB_SHA} in hand). A stampless build is now a configure-time WARNING
   naming the consequence, not silence.

   MOBILEGL_BUILD_STAMP_PRESENT IS MIXED AS ITS OWN FIELD. "this build cannot
   name its commit" and "this build's commit is the empty string" are different
   claims, and collapsing them is how every stampless build in the world agreed
   with every other. The test asserts the flag moves the fingerprint in EITHER
   build state, and the `BuildStamp = ""` perturbation is now guarded - in a
   stampless build production's value already IS "", so that assertion was
   passing while proving nothing.

2. THE CLIENT NEVER READ abiMajor/abiMinor. Hello has carried them since P5
   and the SERVER checks them; the client compared only the fingerprint. The
   fingerprint mixes AbiVersion so a mismatch would fire anyway - but as
   "struct shapes", which sends the reader looking for a layout change that is
   not there. Under spawn the peer is a SEPARATE IMAGE the operator chose with
   MOBILEGL_IPC_SERVER_PATH, so this is exactly the failure that needs its own
   sentence. Both handshake sites call one helper; there are two sites, and
   only one of them was reachable before `sm`, which is how the gap survived.

3. Hello::pid WAS 0 AND Welcome::serverPid ECHOED IT. So both were always 0 and
   neither named anything, while §9.5's arm-proof gate wants exactly the child
   pid. Both now carry real ids, and the pair is the cheapest arm proof in the
   protocol: under spawn they MUST differ, and a same-process session cannot
   produce two different values. The client keeps what the server STATED -
   local launcher knowledge is a different fact from one that crossed the wire,
   and only the second can be asserted from the server's own mouth.

R-16 both ways: revert serverPid to echoing hello->pid() and the new test reds
on both halves, naming 4.3. G1 re-measured after the CMake and header changes:
0 symbol diff, .text unchanged at 0xa52203.

Unit suite goes from two long-standing failures to one - the remaining
PipeCatalogue.FrontendNeverTakesAnApplierAddress is an environmental path
problem in this tree, unrelated. All six OpenRA arms still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 19:21:43 -04:00
swung0x48andClaude Opus 5 e766351299 [MG_Remote, MG_Impl] (P6/dl): the device-lost latch, taken from a descriptor and never from a deadline
Before this, CONTRACT-P6 5.3's own sentence held: "a dead server leaves the
client silently DECLINE-ing every verb forever with glGetError clean".
FatalCode::DeviceLost had zero producers and zero consumers and
glGetGraphicsResetStatus returned GL_NO_ERROR unconditionally, so an
application had no way to learn that the process holding its GPU objects was
gone. Measured cost of that: a server killed on its first Clear left the
client waiting out the full 120 s barrier and reporting Fatal{BarrierTimeout}
- the wrong diagnosis, two minutes late. It now notices in 0.01 s.

THE MACHINERY WAS ALREADY THERE. Doorbell::Wait breaks out on Dead(),
SessionProducer::Park turns that into SessionWait::ShutDown, and ClientSession
already had the named DECLINED path. What did not exist was the death FACT:
D5c's finding is that the client parks on clientBell[0] and rings ITSELF
through clientBell[1], holding both ends, so EOF can never arrive on the one
descriptor it watches. A bell that can wake itself cannot hear a death.

SetDeathWitness(fd) fixes that with no new descriptor: the control socket's
far end is held only by the server. The subtlety is that the bell must watch a
socket it must NEVER read - the transport's reassembler owns those bytes and a
control reply is not a death. So the witness enters the poll set with
`events = POLLRDHUP` and nothing else: POLLHUP/POLLERR/POLLNVAL arrive in
revents unrequested, so the hangup is seen while POLLIN never is. Nothing is
recv'd from it and its ownership stays with SocketTransport.

Dead() AND PeerHungUp() ARE TWO FACTS. Park sets m_dead for POLLERR, POLLNVAL
and unrecognised revents too - each a fault in THIS process's descriptor - and
CondVarDoorbell::Kill() sets it on every orderly Stop(). A latch taken from
Dead() would report a lost GPU for a local fd bug and a context reset for
every clean exit, so only PeerHungUp() arms it.

GL_UNKNOWN_CONTEXT_RESET, not GUILTY or INNOCENT: those assign blame, and the
server died for a reason that never crossed the wire.

D5b, in the control-reply wait, which had the right comment and no code: EOF
on the control stream is device loss; a TIMEOUT with the socket still open is
"ALIVE BUT SILENT" and says so by name. 5.4 forbids calling a frozen or merely
slow peer dead, and under P5e run-ahead a server one frame behind is the
INTENDED steady state - a latch armed by a deadline would fire on a healthy
session under load.

MOBILEGL_IPC_RESPAWN is now a NAMED REFUSAL rather than a variable nothing
reads. The latch is one-way on purpose: every handle this client minted names
an object inside the server process, so recovery means re-pushing the world.

THREE TEST NAMES ADDED, none renamed (G14), and R-16 run both ways:
  * remove the witness -> AKilledServerLatchesDeviceLostFromTheHangupAndNot-
    ADeadline goes red, takes 2 s instead of 0.01 s, and names D5c.
  * collapse PeerHungUp() into Dead() -> ADeadBellIsNotAlwaysAHungUpPeer goes
    red naming the local-fd-bug consequence.
  * AnOrderlyStopIsNotADeviceLoss stays GREEN under that collapse, and its
    comment now says so. The first draft claimed it was the control for it;
    running the falsification proved otherwise - under spawn the self bell is a
    SocketDoorbell and Stop() never kills one. The comment was corrected rather
    than the claim quietly kept.

G1 RE-MEASURED, NOT ASSUMED: 0 symbol diff against the c6 baseline and .text
unchanged at 0xa52203. That measurement earned its keep - it caught a 16-byte
drift that had nothing to do with dl: P6's O_APPEND log-sink fix was compiling
into the PULL build, where a second process can never exist. Now guarded, and
.text is back to the baseline exactly.

Full unit suite: 2350 tests, the same two pre-existing environmental failures
and no others. All six OpenRA arms (2 backends x monolith/inproc/spawn) green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 18:49:56 -04:00
swung0x48andClaude Opus 5 2afa000295 [MG_Remote, android-plugin] (P6): a real phone retraces a real trace across two processes
OpenRA.DirectGLES, transport=spawn, on a Redmi: ssim=1.000000,
mismatchPixels=0, zero Fatal{ lines, three runs for three. The server is a
process the client launched out of the APK's lib/arm64/, and the client's own
log names its pid.

Four things stood between the desktop green and the device green, and every
one of them was invisible on desktop for the same reason: a build tree is not
a deployment, and Linux is not Android.

1. THE SERVER COULD NOT LINK. It links libMobileGL.so and is EXEC'd, not
   dlopen'd, so it gets a fresh linker namespace with the DEFAULT search path
   - and an APK's lib/<abi>/ is not on it. `CANNOT LINK EXECUTABLE ...:
   library "libMobileGL.so" not found: needed by main executable`, before a
   single line of MobileGL ran. On desktop CMake had been giving the target an
   absolute build-tree RPATH, so the WSL server resolved its dependency
   through a path no phone has. $ORIGIN is asked for via BUILD_RPATH (a raw
   -Wl,-rpath,$ORIGIN reaches the generator unquoted and the SHELL eats it -
   the device linker said `normalize_path - invalid input: "$"`), and because
   the NDK drops RPATH from the link anyway, the LAUNCHER states
   LD_LIBRARY_PATH: it is the one component that has already resolved where
   the image lives, and a pair that is not co-located is not a pair.

2. THE RENDEZVOUS NEEDED A WRITABLE DIRECTORY. An Android app has no /tmp, so
   bind() in the child had nowhere to put the node and the client refused by
   name 20 s later. AF_UNIX endpoints now accept the ABSTRACT namespace with a
   leading '@' - the systemd/D-Bus convention - which has no filesystem
   presence at all: no directory, no mode bits, and no stale node to unlink
   after a crash, which is the bind() failure Listen() has been working
   around. Its reach is the network namespace, which for an app is the app.
   StartSpawned mints '@' names; an explicit path still works everywhere, and
   that is what P12's "connect to something somebody else is listening on"
   will use.

3. PBUFFER WAS A DirectGLES-ONLY KNOB. Twice over: trace-replay-ci.sh gated
   `--ez use_pbuffer true` on the backend name, and the JNI demanded a native
   window for DirectVulkan unconditionally. A surface SHAPE is not a backend
   property, and the coupling mattered because the Android GLWS makes a window
   surface whenever gNativeWindow is non-null - so handing Magma a window was
   the same as asking for one. Until P12 a spawned server cannot be given a
   window at all: an ANativeWindow* is a pointer into the CLIENT's process and
   SetWindowHandle is refused on the way across with Fatal{UnmigratedSurface,
   "AndroidNativeWindow@P12"} (Rule H). Which is exactly what happened, one op
   after the server had come up green.

4. THE RUNNER LOOKED IN ONE OF THE TWO OUTPUT DIRECTORIES. assembleTraceRelease
   writes apk/trace/release; CI has staged the same artifact under
   apk/trace/debug. A tree that has built locally has both, and picking the
   wrong one retraces last month's APK and says nothing about it. Newest of
   either, now.

MEASURED AND NOT FIXED: DirectVulkan on this device diverges under BOTH split
transports and does so nondeterministically - inproc scored ssim=0.988998 /
9836 px, spawn scored 0.976494 / 14658 px and then 0.988998 / 9836 px on a
repeat, while monolith with the same pbuffer surface scores 1.000000 / 0 px.
The two transports draw from the same set of outcomes, so this is the split
path on Magma against a real GPU, not the process boundary: P6 neither caused
it nor can fix it. DirectVulkan spawn IS green on desktop (lavapipe, 1.000000).

G14: one test name added, AnAbstractEndpointNeedsNoWritableDirectoryAndLeaves-
NoFile, which pins both halves - the handshake crosses an abstract name, and
nothing is left on the filesystem. A '@' mistaken for a path would have bound
a file of that literal name, passed here, and failed on the one platform this
exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:33:57 -04:00
swung0x48andClaude Opus 5 ea9fc7fa71 [MG_Remote] (P6/cp): a real trace retraces across two processes, pixel-for-pixel
OpenRA.DirectGLES.SPAWN is green: ssim=1.000000, mismatchPixels=0, zero
Fatal{ lines, against a server in a process the client started by name. The
inproc SPLIT arm stays beside it as the control, so "did the transport break
it" and "did the phase break it" remain separately answerable.

Five defects stood between the handshake and that picture. Each one was
silent in its own way, and none of them looked like what it was.

1. THE DOORBELL COULD NOT WAKE ITSELF. SocketDoorbell's one-fd form sends on
   the end it polls, so a write is delivered to the PEER - correct for waking
   the other process, useless for waking a thread in this one. The server's
   control pump posts into the apply thread's mailbox and must ring the bell
   that thread parks on. Symptom: every eglInitialize crossed, landed in the
   mailbox, and timed out at 5 s. The two-fd form (park on [0], notify on [1])
   is restored here, and the bell handover goes from two descriptors to THREE
   - the client needs the same split for the same reason. Seven offers now.

2. InitCapabilities HAD NO WIRE KIND. CONTRACT-P6 §2 predicted this exactly
   ("the request has a live client caller and the codec refuses to encode
   it"). SurfaceOpKind gains 11, append-only; the decode range's upper bound
   moves with it, or the new tag reads as out-of-range and surfaces as
   Fatal{ProtocolCorruption} rather than the missing row it is. The op's name
   loses its InprocOnly suffix, which stopped being true here.

3. THE CHILD LOADED NO CONFIG AT ALL. mobilegl_server_main calls
   InitServerRoleForSpawn directly, and MG_ConfigLoader::Init is a step of
   MobileGL::Initialize - so every MG_Config value in the server was its
   static default. MOBILEGL_BACKEND_TYPE survives the launcher's scrub on
   purpose and was read by nobody: a DirectVulkan session would have brought
   up a DirectGLES server and then disagreed with its client about which
   backend the capability mask describes.

4. AND THEREFORE Transport READ AS Monolith, which CONTRACT-P6 §3.1 (D1)
   forbids in as many words. a6 measured the cost: 226 live lines test
   `Transport != Monolith` to select the SERVER arm, 148 in MG_Backend.
   Monolith flips all of them to frontend glue in the one process with no
   frontend. Observed precisely as predicted - UnitTexturesByHandle() answered
   false, Clear took the legacy arm that dereferences borrowed FRONTEND slot
   pointers, and the apply thread took a SIGSEGV inside SyncNeccessaryTextures
   on the first Clear record of the trace. It is pinned in-process rather than
   through the environment because the environment is where it must NOT be:
   LooksLikeAClientEnvironment exits 65 on a surviving MOBILEGL_TRANSPORT, and
   that check is anti-recursion catch (b). Dial, not Transport, is that axis.

5. THE LOG WAS CORRUPT, NOT MERELY INTERLEAVED. Two processes opened the same
   MOBILEGL_LOG_FILE_PATH with "w" and each carried its OWN file offset, so
   they overwrote each other's bytes - half-lines like `ote client: CapsMirror
   generation 1 adopted`, and the server's whole startup block vanishing under
   the client's later writes. This cost real diagnosis time twice, because a
   missing line read as a code path not taken. O_APPEND now; the client still
   truncates once so a fresh run is not a pile of old ones, and the server
   never does - it would delete the very startup lines a failed session needs.
   run_trace_case.cmake's Fatal{ census reads exactly this file, so it was
   also reading a lie.

The retrace runner's split assertions become transport-aware: one row per
transport it admits, and a transport with no row is a FATAL rather than a
fall-through, because the fall-through is the accident the block exists to
prevent. The spawn row carries a SECOND sentence and it is the stronger half
- ConfigLoader's marker proves the value RESOLVED, while `spawn ARMED - the
server role runs in pid N` is written only after the launch, the connect and
the handshake all succeeded, and carries a pid that is not this process's.
inproc resolves its own marker while running the server role on a thread
here; nothing on that path can write this one.

G14: one test name added (TheEleventhFramedOpIsInitCapabilitiesAndItCrosses-
TheWire), none removed. The ten-op table keeps its name and its scope rather
than being renamed to eleven.

Two unit failures predate this change and are environmental in this tree:
PipeCatalogue.FrontendNeverTakesAnApplierAddress (the frontend source root
resolves empty) and SessionHandshakeTest.TheAbiFingerprintChangesWhenAnyOf-
ItsInputsDoes (GIT_COMMIT_HASH_SHORT is empty here, which a6 already recorded
as red in this configuration).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:23:41 -04:00
swung0x48andClaude Opus 5 b0f4f8b744 [MG_Remote] (P6/cp): EGL control frames cross to the other process, and answer
An eglInitialize issued in one process is dispatched in another and its answer
comes back. That is the last thing standing between the session and a native
context, because the server creates its context on the apply thread when the
client's first eglMakeCurrent crosses - and until now nothing crossed.

ONE SEAM, NOT TWELVE. a6 row A4-13 counted twelve Server* forwarders, each
ending in ServerLoopInstance().RunSurfaceControlFrame(frame) with no role
branch, and concluded twelve edits. They all funnel through that one function,
so the branch goes there and the forwarders are untouched.

A HOOK, NOT A CALL. ServerLoop::SetRemoteControlSink takes a function pointer
the client installs at handshake. The obvious alternative - the server half
asking ClientSession::Active() - would have added a fifth symbol to the six
MG_Remote already owes itself (a6-link-experiment §5), in the one file with a
clean reason not to.

The branch is BEFORE the local seq mint, deliberately. RunSurfaceControlFrame
numbers unnumbered local posts; a wire request must keep the CLIENT's number all
the way through dispatch and reply or its answer cannot be correlated
(CONTRACT-P6 §6.3, which says the client mints, always).

The client sink encodes with EncodeSurfaceOpFrame, sends, and waits for the
SurfaceReply carrying its own seq - under one mutex, because the twelve
forwarders are already caller-serialised on the server's m_callerMutex under
inproc and two replies racing for one correlation is the same bug on either
side. A CapsSnapshot arriving while it waits is ADOPTED rather than dropped:
the server republishes caps from inside its MakeCurrent dispatch (a6 row A4-30)
so a snapshot legitimately precedes the reply to the op that produced it. The
wait is bounded and its expiry is NOT fatal - a peer that is gone is a different
fact from a peer that is slow, and only the doorbell's death latch may say which.

The server's pump decodes and applies through ServerApplyWireSurfaceOp, which
a6 found had no production caller anywhere. It runs on ServerMain's own thread -
NOT the apply thread - which is what §6.2 asks for: a control arrival must not
wake the applier, and frame decode must not happen on the thread holding the
native context. A frame that is not a verifiable SurfaceOp ends the session
rather than being ignored, because a silent drop is the failure mode this
project refuses everywhere else.

THE TEST'S ASSERTION IS THE OUT-PARAMETERS, not the EGL result. A headless
machine has no display to initialise, so the op is expected to be refused - and
ServerInitializeEGLDisplay writes major/minor "whenever the dispatch ran,
success or not" (the forwarder's own comment). They move off -1 if and only if
a SurfaceReply came back from the other process; a transport failure returns
before the write-back. I got this backwards first and the test said so.

Gates: ServerSpawnTest 5/5; unit suite 2345 with the same two pre-existing
environmental failures; G1 pull 0 added / 0 removed, .text 10822147 unchanged.

Not TCP. It was considered and dropped for now: TCP has no SCM_RIGHTS, so the
four segments cannot cross it, and a TCP control plane would have to refuse a
data plane by name until StreamLink exists (P6.5). Recorded here so the reason
is on file rather than re-derived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:50:28 -04:00
swung0x48andClaude Opus 5 207c31f6db [MG_Remote] (P6/sm): two INDEPENDENT processes, joined by a name, sharing memory
The cross-process session is up. A server process is started on its own with an
endpoint and nothing else, it listens; a client started separately connects to
that name; Hello crosses, Welcome comes back with four SegmentRefs, six
descriptors cross by SCM_RIGHTS, and the client maps memory another process
created. ClientSession::Active() is non-null at the end of it, which is the flag
that arms the thirty-seven wire emitters.

INDEPENDENT, NOT FORKED - and that is a correction to this package's first
shape. The version committed an hour ago forked the server and handed it fds
3..6. It worked, and it was less code, but inheritance can only ever produce a
server that is a CHILD OF ITS CLIENT, which is the one arrangement the end state
cannot use: there the server is a standing application and the client is
somewhere else entirely, possibly on another kernel. CONTRACT-P6 §3.1 called
this Dial == Connect and filed it as a later shape. It is the primary one, and
fork/execve survives only as a way to START a process - every descriptor it used
to carry is gone.

What that costs and buys, concretely: the rendezvous becomes a NAME, so
SocketTransport grows Listen/AcceptPair/ConnectTo, with a bounded connect retry
that is the CLIENT's (a launcher that waited for the server to be ready would be
coupling the two again by the back door). The bells stop being inherited fds and
are created by the server and handed over with the segments - slots 4 and 5 of
six SCM_RIGHTS offers. And the client connects TWICE to the same path: an fd
offer is a sendmsg whose ancillary data rides with specific bytes, so it may not
share a socket with the framed control stream, or the frame reassembler and the
descriptor receiver race for the same bytes.

The pieces behind it:
  - SessionSegments::AdoptFromDescriptors, which is the same Adopt+Map loop
    AttachInProcess has run on every inproc session since P5. That was the point
    of writing the inproc attach as dup+Adopt+Map rather than as an alias: only
    the origin of the descriptor is new.
  - ServerSession::SetExternalDoorbells, the narrow fix for a6 row A7-1 - the
    §3.9 accessors abort for every role but InProcess, so a spawn apply thread
    died before popping its first record. lk deletes them; until then an injected
    pair is honest about where the bells come from.
  - MG_Backend::InitServerRoleForSpawn: steps 1 and 2 of the split bring-up
    extracted so the server process runs the SAME code the inproc server role
    runs. The capability bits are a statement about the server's own backend and
    a second copy would diverge the first time a backend registers a slot.
  - ClientSession::FinishStartup: everything below the attach - producer, reply
    pool, event ring, segment table, encoder, first CapsSnapshot, emitter tables -
    shared verbatim between inproc and spawn. Its one parameter is not a
    transport question: under inproc the client starts the apply thread because
    there is no server-side main; under spawn there is one, and it started the
    thread before it sent the first descriptor.

G1 CAUGHT A REAL LEAK on the way: InitServerRoleForSpawn was guarded on the
inside, so the pull build still DEFINED the symbol - 2 added, 1 removed, .text
+16 bytes. The whole function and its declaration are now inside the guard.
"It returns false there" is not the same as "it is not there", and the gate is
what said so.

Gates: ServerSpawnTest 4/4; unit suite 2344 with the same two pre-existing
environmental failures; G1 pull 0 added / 0 removed, .text 10822147 unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:36:14 -04:00
swung0x48andClaude Opus 5 f4cbaa8945 [MG_Remote] (P6/lk): regroup RingControl by writer, and make the asserts that pin it falsifiable
The shared control page both roles map had a mixed-writer cache line:
{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial, presentAckSerial}
sat together, but submittedSeq is producer-written (Ring.h's "THE FIVE
WATERMARKS") while the other four are consumer-written. That is why no
single-writer progress block could be aliased over it, and it is what this
package fixes: submittedSeq moves up beside cmdHead, the other thing the
producer writes, and the four that remain become a named LinkProgress member -
the struct c6 declared in ILink.h - so a data plane with no shared page can own
the same four values and every wait predicate in SessionRings.h keeps ONE
implementation at source level.

The two event flags deliberately stay in the doorbell group. Promoting them
would put the client's per-drain exchange(0) on eventRingFull (EventRing.h:200)
onto the line the client's own spin predicate reads 1e5-1e6 times a frame.

73 call sites rewritten, 15 in the library and 58 in tests. The rewrite is
receiver-scoped rather than field-scoped, because SplitRuntimePeek.h:77 has its
own struct with a field of the same name and SplitRuntimePeek.cpp:146 has both
on one line - a field-scoped rename would have silently broken the peek struct
that MagmaRunAheadScenario reads nine times.

THE ASSERTS ARE THE PART THAT NEEDED A SECOND PASS. The first revision shipped
four aggregate asserts and an adversarial review broke them by counterexample:
swapping retiredSeq and completedFrameSerial inside LinkProgress compiled
clean, passed all four, and passed the runtime layout test too - because
appliedSeq stays at offset 0. Three wire-visible offsets on a shared page were
pinned by nothing. Nothing else in the tree covers it either: the ABI
fingerprint takes no input from this page, and sizeof(RingControl) is 4096
whatever the members do, because alignas(4096) makes it so.

So the block is now per-field as CONTRACT-P6 §8.3 actually asked, plus a
standard-layout precondition, a tail bound that alignas cannot satisfy for us,
and the LinkEventFlags mirror. Each was run red: the field swap reds the
per-field asserts by name, a fifth member inside LinkProgress reds sizeof.

One of them cannot go red, and Ring.h now says so rather than implying
otherwise. A SMALL member inserted between Progress and serverEpoch lands at
offset 288, inside Progress's own line, and serverEpoch's alignas(64) absorbs
up to 32 bytes of it - so both line asserts still pass. The review's proposed
fix does not catch it either; I tried it. A gate nobody can falsify is worse
than no gate, so the limit is written down beside the asserts.

Also: RingTest.ControlPageLayoutIsTheSharedContract said nothing about the one
field this package moved, and now asserts the producer/consumer separation and
the event flags' exclusion itself rather than borrowing the claim from Ring.h.
Ring.h grew 30 lines, so the Ring.h:NN citations this package and c6 authored
are re-anchored to named blocks instead of line numbers. ARCHITECTURE.md §11.2
is updated for the new layout, and a factual error introduced in the previous
revision of that paragraph - submittedSeq annotated as releasing staging, which
retiredSeq does - is corrected.

Gates: pull 0 added / 0 removed, .text size 10822147 unchanged; split 0/0,
.text size 11681395 unchanged; unit suite 2326/2328 with the same two
pre-existing environmental failures as the baseline. The five edits behind
MGITEST_SPLIT_RUNTIME_PEEK were proven compiled by a red-once, not assumed.

Note on the .text claim: that is a SIZE, not the bytes - field offsets moved,
so immediates moved with them. And this page's layout IS observable across
binaries; what is unobservable is behaviour within one binary. The contract
already books the layout change deliberately, before spawn exists to care.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 09:23:48 -04:00
swung0x48 68b55ea371 [MG_Remote] (Disaggregated): the inproc barrier watchdog is a deadlock detector, so its budget leaves room for a leg
lavapipe/llvmpipe stall the apply thread for tens of seconds inside texture-handle
registration (vkCreateImageView -> lvp_CreateImageView -> llvmpipe_register_texture,
a JIT/futex path): one measured stall is 29.3-39.7 s and a whole inproc retrace run
accumulates ~44.6 s of it. At a 30 s bound those runs red intermittently on
Fatal{BarrierTimeout, "ResourceRespecify"} while the same stall under monolith is
merely slow - monolith has no cap on that path. Since the monolith shares the stall,
the bound, not the workload, was the defect.

kBarrierTimeoutMs becomes 120000, >3x the largest observed stall, which still leaves
the watchdog job: a record that is never retired still terminates the run and the
Fatal still names the seq. The comment now says deadlock detector rather than
performance gate and carries the measurement.

The fake-clock case that pins the budget moves with it: FenceWaitBudgetHonorsGl-
TimeoutAndFiniteTransportChunks expected 30000/30001/90000 and now expects
120000/120001/180000, so the assertion still pins the value instead of drifting.
Texts that wrote the old number down are updated: ARCHITECTURE 17.1, CONTRACT-P5E
2.4 (which now names kBarrierTimeoutMs rather than the literal), and the two
bring-up comments in PipeRoute.h / WireTables.cpp that dated a self-wait to
"thirty seconds".
2026-09-21 02:54:47 -04:00
swung0x48 1e7c372e6c [MGPipe] cut buffer content ranges at the stage segment's chunk budget
One resource_subdata record's blob is staged whole in SEG_STAGE, so a range
larger than that arena is Fatal{RingOverrun, SEG_STAGE} at the encoder rather
than a split: the CI traces abort there on a 128 MiB arena's whole-buffer
follow-up against a 32 MiB segment.

- ClientSession::StageCapacityBytes(), the twin of EventRingCapacityBytes, so
  the emitter can size against the arena the codec allocates from.
- MGPipeStageChunkBytes(): clamp(segment/4, 4096, segment), 0 when there is
  nothing to fit (monolith, the server role's own uploads, no session).
- PipeFill.cpp's two buffer content walks take that cap instead of the
  record's own 2^32-1 bound, so an over-long range becomes several contiguous
  ascending records. StagedShadowStore::Adopt already accumulates coverage.

ClientSession.h/.cpp, GpuWritePending.h/.cpp, PipeFill.cpp.
2026-09-20 18:45:24 -04:00
swung0x48 51e61ddabe [MG_Backend] (Disaggregated): order Magma GPU work and defer wire object retirement 2026-09-20 04:11:36 -04:00
swung0x48 35ad51d8f7 [MG_Remote, MG_Test] (Disaggregated): P5f fs - keep applier diagnostics within each role 2026-09-19 23:49:45 -04:00
swung0x48 2d03565f87 [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.
2026-09-18 13:38:40 -04:00
swung0x48 48b2f56ef8 [MG_Remote] (Disaggregated): P5e ra - the run-ahead latch goes down with the session
- CONTRACT-P5E §1. The latch means "what THIS session's first caps adoption said", and
  ClientSession is a process singleton that Start/Stop can cycle. Left standing, a second
  Start would inherit a verdict about a server that is gone - and because a later
  snapshot may only turn run-ahead OFF, an inherited `true` could never be corrected by
  the honest answer arriving afterwards.
- The present serial resets with it: a credit's id space belongs to the session that
  paces on it, and a server that never saw frame 1 cannot return its credit.
2026-09-18 08:27:00 -04:00
swung0x48 6e6b86e592 [MG_Remote, MG_Pipe, MG_Impl, MG_Backend] (Disaggregated): P5e ra - the wait rule, the present credit and server-owned gPipeInputs
- CONTRACT-P5E §2.3. EmitAndWaitTails now switches on the generated WaitClass column
  when RunAheadArmed(): kWaitNone and kWaitPresent return at the publish, kWaitApplied
  waits and then releases the fill pins, and a reply row is unchanged. It REPLACES the
  BatchWaits skip rather than layering on it - the batch rule reads the call CLASS and
  this one reads what an apply READS, and the server's MGPipeBarriered reads the second,
  so two rules layered is two halves of the wire disagreeing about whether the client is
  parked.
- §1: RunAheadArmed() = barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply),
  latched at the first caps adoption after Start and demotable but never promotable - a
  record already published without a wait cannot be un-published. A knob set against a
  server that does not publish the bit logs once and runs lockstep.
- §2.4: MGPPresent::FrameSerial is minted client-side, 1-based, by AcquirePresentCredit,
  which pays the credit BEFORE the encode (a client that encoded first would hold a
  SEG_CMD reservation across the park). ServerVerbSink::OnPresent returns one credit per
  swap through ServerSession::ReturnPresentCredit, which has had no production caller
  since v1 wrote it; serial 0 on a run-ahead server is Fatal{ProtocolCorruption,
  "Present.FrameSerial"}. The stats line and the wire ledger gain credit-waits.
- §2.5, the drains and the forced waits: every wait exit drains SEG_EVENT (the applied
  wait, the present-ack wait, the cmd-space wait and - through a hook the session lends
  the encoder - the stage bell); glFinish becomes WaitForApplyToCatchUp, since "the
  commands issued so far have completed" is a promise a queue nobody waits on can break;
  every Server* EGL forwarder waits first, because the control mailbox is pumped BETWEEN
  drain batches and a make-current landing between two run-ahead records is not a wrong
  pixel but a wrong everything. ServerSwapEGLBuffers is not on that list: present is the
  swap.
- §2.5 / ruling 15 (ID-93): resource_subdata's BUFFER half stops waiting. The row keeps
  kReplySlot, the server still posts, and the slot is never read - which is what makes
  the 64 KB persistent-map push fire-and-forget instead of a hidden round trip per block.
  MGPipeSubDataWantsItsReply is the one predicate, read by the route and by the emit
  table, so the two cannot disagree about whether a wait is owed. The texture half keeps
  its reply: DrainTextureSubData clears a level's dirty flag on the accepted answer.
- §3: gPipeInputs becomes server-role memory for unbarriered records. The tracker walk
  and the emitters still run - they produce records - but the serial bump, the verb
  stamp, the server-stamp withdrawal and the whole of step 4 do not, and
  MGPipeNoteFrontendMutation / MGPipeLeaveVerb return instead of writing. `fillOwed` is
  a separate name from `barriered` on purpose: the red-once is that one line.
- §3.3: MGPipeInputUnfreshRead's counter becomes an UNCONDITIONAL abort under an
  unbarriered record, knob or no knob, because the value is torn or stale by
  construction and there is nothing for a counter to count. The marker gains its reason
  so the lane's allowlist can tell the two apart.
- §3.5: RefusePipeInputsTouchWhileApplierOwnsIt is re-derived - under run-ahead the rule
  is "not at all unless you are the barriered fill", and the BatchWaits early return
  goes with the argument that justified it.
- §2.3's ReleaseFillPins drops the four O-class SharedPtr rows once a barriered apply
  returns: under lockstep the next fill overwrote them within a verb, under run-ahead a
  pinned VAO can make the apply thread its last owner a frame later.
- §2.7 / ruling 13 (ID-91): the deferred-destroy queue stays live, and an enqueue from
  an UNBARRIERED apply is now a named finding - loud once, Fatal under strict.
- NAMED DEVIATION from §3.1: the identity/liveness pair is still written on every verb.
  It is not a field of the residual model - no ownership row owns it, no record carries
  it, no server stamp can answer it - it is "does this process still have a GL context",
  which only this thread knows and every null-context guard reads.
- Inert until the integration commit: RunAheadArmed() is false while
  kMGPipeP5eRunAheadReady is, so every branch above answers as it did before.
2026-09-18 07:58:06 -04:00
swung0x48 1f8de61be1 [MG_Remote, MG_Pipe, MG_Impl, MG_State, MG_Backend, MG_Test, MG_IntegrationTest] (Disaggregated): P5d round 3 - the profile-directed cuts on both roles
- The round-2 report guessed the apply thread's ~48% mutex time was the backend
  pendingMutex; the symbolized call graph at 56a77348 says it is the idle poll in
  ServerLoop::ApplyThreadMain taking m_controlMutex twice per iteration
  (ControlIsPending in the ready lambda, PumpControlRequest), with DrainRing at
  29% of the thread. Every package below is cut from that graph, not from self%.
- T (transport): the control mailbox gets an atomic shadow bit m_controlPosted so
  the idle poll takes no lock (handshake, C2 critical section and the
  NOT_INITIALIZED answer unchanged); Doorbell::Wait spins on a once-calibrated
  iteration budget and reads no clock in steady state (the park phase alone
  computes deadlines); client/server wait and park counts ride the stats line
  as wait[srv= srvpark= cli= clipark=] and the wire ledger as cliwait=/clipark=.
- D (role guards): OnApplyThread() is inline - one relaxed atomic key against the
  caller's thread pointer (aarch64 __builtin_thread_pointer, else
  std::this_thread::get_id) - and m_applyThreadId, the function-static guard and
  the pthread_equal call are gone; the three scope depth counters stop being
  thread_local (their only reader is the apply-thread guard, so they count only
  when OnApplyThread() is true; a GL-thread scope no longer exempts the apply
  thread, pinned by a new case); the guard chains are ordered cheapest-first.
- B (persistent maps): the shadow allocator rounds the SIZE to a page multiple, so
  TrackWriteMap aligns OUTWARD when the shadow is page-granular - a sub-page map
  is one protected page, edges and their per-draw XXH3 disappear; the inward arm
  stays as the fallback. Review found a hole the edge hashes had hidden since
  round 1: a map that faulted but was not consumed by the walk that advanced the
  epoch was never pushed. The walk now drains every marked member before the
  binding walk, registration bumps the epoch so a fresh map ships once on the
  draw path, and a non-4 KB kernel page refuses the mprotect arm by name.
- C (client per verb): an image-unit high-water mark (push build only, fed by
  glBindImageTexture) replaces the 192-unit sweep per draw; the residual fill's
  7-term supplied conjunction is a memo keyed on its four inputs; the server's
  63-field stamp per verb is a constexpr per-class mask table.
- generate_mipmap no longer skips its wait under MOBILEGL_IPC_BATCH_WAITS: its sink
  is the backend GenerateMipmap, which reads MGB_CTX->GetActiveTextureUnit(), and
  a batched publish let the next validate move the serial under it -
  Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@ReadPixels"} on the gate tree.
  The rule is written at the site: a record may skip its wait only if its apply
  reads nothing the residual fill writes. The texture-death scenario gets the
  framebuffer case's fence before it reads the object_death tally.
- Redmi 2f7cbe2e, FCL + Minecraft 26.3-rc-3, VD12, CPU pinned: inproc 64.3 fps
  (client 15.0 ms/frame, apply 12.4) -> 103-114 fps (client 8.7-9.2 ms/frame on a
  mid core, apply 7.0-7.4); monolith 115 (120 Hz vsync-capped runs) / 206 (one
  uncapped run, 5.0 ms/frame on the big core). Gates: unit 2203/2203,
  integration-split 111/111 (the two formerly flaky cases 6/6 on repeat),
  generators, include closure, dirty surface and doc citations green.
2026-09-18 04:28:25 -04:00
swung0x48 cb06538c06 [MG_Remote, MG_Pipe, MG_Impl, MG_Test, MG_IntegrationTest, Docs] (Disaggregated): inproc performance package - dirty-driven persistent-map push, batched barrier waits, spin-clock batching
VD12 Minecraft on the Redmi (FCL, DirectGLES): the split's in-world frame rate
13 -> 60-66 fps against monolith's 110-114, and persistent-map wire bytes
25 MB/frame -> ~0.23 MB/frame. The residual split-specific costs are named at
the end, not hidden: the R-1 serialization (WaitForApplied ~12%, P11's
versioned-inputs design) and a split-only mutex complex still under
investigation.

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

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

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

Gates: gen_pipe --check/--self-test green; unit 2188/2188;
integration-split 110/111, the one red being GenerateMipmapDepthPixels whose
DirectGLES and DirectVulkan MONOLITH siblings flake identically with every
code path of this package provably inert (PushIsArmed false, no ClientSession,
deferral's monolith early-out).
2026-09-18 01:58:59 -04:00
swung0x48 b88e848761 [MG_Remote, MG_State, MG_Impl, MG_Backend, MG_IntegrationTest] (Disaggregated): P5c census triage - the three real regressions the merge owed
The broad inproc census against a same-machine 11ac3de6 baseline found 183 newly-failing;
172 are the design-red the contract rules (the legacy-arm refusal under transport: the
subsystem-off and legacy control lanes, and Magma's P7 surface - every one carries its
Fatal name in ~/p5c-fatal-map.tsv), and the other eleven were three REAL regressions,
fixed here:

1. The surface-changed event drained at the first verb's EmitAndWait, but a pre-verb
   glGetFramebufferAttachmentParameteriv on the default framebuffer was answered from the
   placeholder attachments, and buffers allocated from that answer are blit-incompatible
   with the real surface (DepthStencilReadbackAttachmentShapeScenario). The event ring
   gains its SECOND drain-point class: the blocking EGL lifecycle RPC returns
   (BackendObject_Remote's surface create/resize and make-current) - the same known-idle
   premise as EmitAndWait's post-barrier point. CONTRACT-P5C section 4.1 amended.
2. A whole-buffer writeback larger than the event ring could never fit one record
   (capacity/2), aborting the server with Fatal{EventRingOverflow} on the large-arena
   readbacks. The client now slices the request at a quarter of the ring (header and
   co-posted small events ride along), each slice round-tripping its own barrier + drain;
   the whole-buffer flag is cleared by hand after the last slice lands, which is exactly
   what WritebackFromBackend's clear says. CONTRACT-P5C section 4.4 amended.
3. ScatterCapturedRecords / ReadbackCapturedRanges read the capture buffers through the
   frontend MappedData() (a layer-1 surface since gt) and wrote back by direct call. The
   XFB capture path now reads the server's staged shadow (RequireStagedCoverage asserted,
   syncedChangeSerial bumped so no later draw overwrites the capture with stale shadow)
   and writes back through OnBufferWriteback, the Ops_H_Readback order.

CtWireScenario's two death cases skip on DirectVulkan by name: object_death's producer is
the Espryt-side death-notice ops and Magma installs none until P7.

Evidence: unit 2187/2187; integration-split 111/111; the three families' named cases green
again; census newly-failing reduced to the evidenced design-red set.
2026-09-17 13:13:41 -04:00
swung0x48 a0865b00f4 [MG_State, MG_Backend, MG_Remote, MG_Impl, CI] (Disaggregated): P5c gt - the role guards close the phase
Layer 1 arms the texture object surfaces at MipmapStorage (the one point every concrete
texture class and view forwards through): AllocateStorage, TruncateMipmapLevels,
UpdateMipmapSubData, MapMipmapData, MarkStorageDirty(Region), IsStorageDirty,
GetStorageDirtyRegion(s) take Fatal{RoleViolation, texture-legacy-arm} from the apply
thread with an active transport. What the lane then exposed is fixed, not exempted: the
completeness gate answers from the staged store's defined-ness, the two logging reads go
through the descriptor macro, and the remint replay loop keeps its named
Fatal{UnmigratedEmulation} ahead of the guard. Magma's texture sync read path is the one
named exemption (MGPipeTextureLegacyArmScope, P7). Layer 2 wires InBarrierWait as the
client half of the gPipeInputs single-writer rule (RefusePipeInputsTouchWhileApplierOwnsIt:
applier owns it AND this thread not in a barrier wait => Fatal{RoleViolation,
gPipeInputs}; the barrier-off negative control is deferred to its own Fatal), and stale
reads of RECORD-SUPPLIED / APPLIER-DERIVED fields under a server stamp now report
RoleViolation with the field named. The RemoteGuards suite pins all nine texture surfaces
plus HasDefinedContent by name, and a negative control for the fill window. CI gains the
integration-split-strict job: unit lane as the hard gate under MOBILEGL_IPC_STRICT_ERRORS=1,
the scenario lane expected-red with the marker asserted (rewritable to plain green once P7
retires the object-class rows). rsp is per-frame in the stats line and measured on the
locally available traces (bsl in-world 948.5/frame avg over 123 frames, complementary
1591.9, iterationrp 286 until its named remint-pull Fatal); value-class pulls are zero by
rv's pinned list, what remains is the object-class fifteen.

Evidence: unit 2187/2187 (also under STRICT_ERRORS=1); integration-split 111/111; red-once
per layer (the texture guard and the gPipeInputs guard each turned their named death test
red and restored green). CONTRACT-P5C sections 5.4, 6.
2026-09-17 11:20:44 -04:00
swung0x48 930ad0193f [MG_Remote, MG_Impl, MG_Backend] (Disaggregated): P5c ev - SEG_EVENT is the only reverse channel
The three reverse MGPipeCallbacks entries become the SERVER session's producer callbacks
(Reserve + fill the head + copy the payload inline + PublishEvents; the host pointer never
reaches the wire), installed at Accept and uninstalled at Close, a second installation
over a claimed entry being Fatal{RoleViolation, callback-double-install}. Producers wired:
buffer writeback (the mapped bytes are copied into the record's inline tail instead of a
raw pointer in MGPBlobRef::Offset), GPU-written (Magma's three direct MarkGpuWritten
bypasses are routed through the callback), surface-changed (the backend posts
MGPSurfaceInfo; the client consumer replays the allocate/format writes against
pDefaultFramebufferInfo on the GL thread, where R3's ownership always was), and
kEventGlError = 4 for PipeInputs::RecordError (ordering stays P9's).
PipeInputs::InvalidateCompileEnv's forward is deleted with an active transport - R-12's
caps re-publication already does its job. The consumer's monolith guard is replaced by
the three segment arms (kSegEvent resolves through the session's SegmentTable;
kMGHostSpanSegNone is monolith-only and Fatal{ProtocolCorruption} on the wire; anything
else the same), and DrainEventRing calls the client consumers BY NAME, never through the
global table. Overflow is Fatal{EventRingOverflow} - P5c's events are lossless, P9 owns
the drop policy. Monolith arms keep the pre-P5c expressions character for character.

Evidence: unit 2139/2139 (incl. the new kEventGlError round-trip), integration-split
107/107, monolith GPU subset 24/24; negative control executed - a writeback blobref
reverted to the raw-pointer shape turns DirectGLES.Split.AtomicCounterScenario.
SubDataAfterDispatchSurvivesAnImmediateReadback red with Fatal{ProtocolCorruption,
OnBufferWriteback.Seg}. CONTRACT-P5C sections 1, 4.
2026-09-17 03:55:30 -04:00
swung0x48 82683d4a83 [Fix] (Remote): honor fence wait budgets and reject invalid readback status 2026-09-16 14:12:14 -04:00
swung0x48 12905a7eb8 [Merge] (P5b): land r1 core fixes with multi-tail command-ring retry 2026-09-16 13:41:40 -04:00
swung0x48 db8b347952 [Fix] (Remote): preserve server ownership and wait for ring retirement 2026-09-16 13:39:50 -04:00
swung0x48 9606466ab6 [Merge] (MG_Remote, P5b): combine d1 draw migration with f1 i1 t2 and r2 2026-09-16 13:30:01 -04:00
swung0x48 d50183cb1a [Fix] (IntegrationTest): assert actual staging backpressure in the small ring lane 2026-09-16 12:34:33 -04:00
swung0x48 e564939615 [Feat] (MG_Remote, P5b d1): flip the nineteen indexed / instanced / multi-draw / indirect draw slots to class B on draw_vbo - the emitters plan the record from the GL call and the bound element / indirect / parameter buffers, stage a client index array as the kDrawHasUserIndices span, refuse +CLIENT_INDICES / +CLIENT_COMMANDS / +INDEX_OFFSET by name, and the sink dispatches every shape to the backend slot the monolith calls (a base vertex of 0 is DrawElements, never DrawElementsBaseVertex with 0: ANGLE has no glDrawElementsBaseVertex) 2026-09-16 12:10:38 -04:00
swung0x48 bd11ab189f [Merge] (MGPipe, P5): land x2 - the E2 draw-drop control, R-10 maxrec and the small-ring wrap counters, resolved against j0 (ID-65) 2026-09-16 11:30:12 -04:00
swung0x48 9f52878313 [Feat] (MG_Remote, MG_Util): the wire ledger - maxrec/maxcap/ringwraps/ringpads/ringwaits on the MGPipe stats line and at session teardown, MOBILEGL_IPC_E2_DROP_DRAW beside the clear drop, and E3(a)'s named persistent-map-push-disabled line (ID-65) 2026-09-16 11:09:09 -04:00
swung0x48 6d86f9d9be [Fix] (Client): share teardown refusal and gate live wire producers 2026-09-16 10:16:11 -04:00
swung0x48 5682f429d0 [Fix, Test] (MG_Remote/Client, MG_Impl/Pipe): c1 round 3 - route the five by-address delete sites (B1); observe the client arm (B3); readback requires Status=OK and the exact extent and refuses a pack-PBO read by name (M2/M8/codex11/ID-57); one ERROR->Fatal{ReplyError} rule across the two escapes and an honest ShutDown decline (M4/codex5); InitialBytesNotCarried is role-aware (M5); a routed call during teardown refuses by name not runs the applier (codex4); caps adopted on every make-current (codex12); set_dynamic_state carries an optional blob; PACK_SKIP_IMAGES ignored for a 2-D read (codex6); one tight-size function for production and test (M3/codex10); redcheck 9/9 red each on its own string (M6) 2026-09-16 09:11:47 -04:00
swung0x48 41992649c9 Merge branch 'feat/disaggregated' into p5/c1
# Conflicts:
#	MobileGL/MG_Remote/Client/ClientSession.cpp
#	MobileGL/MG_Test/Wire/CMakeLists.txt
2026-09-16 07:23:35 -04:00
swung0x48 02f15bb675 [Feat] (MG_Pipe, MG_Remote/Client, MG_Impl/Pipe): R-17 - install the two generated MGPipe tables over the 37 apply entry points, 40 call sites onto the thunks, 37 wire emitters, the reply mailbox, and ID-47/ID-49 readback 2026-09-16 07:10:51 -04:00
swung0x48 949ba509f3 [Fix, Test] (MG_Remote, MG_Test, CONTRACT): wave 1.5 s1 - ID-47 SEG_REPLY is 16 MiB / 8 slots of 2 MiB and ReplySlotPool::CanHold+RequireReadPixelsFits refuse an oversize ReadPixels at the CLIENT by name (ProtocolSmokeTest pin, CONTRACT row 23); CapsAbiFingerprint IS MixAbiFingerprint over CapsAbiFingerprintInputs and the sensitivity case drives it; the null-union Hello/Welcome frames go THROUGH Accept/StartOverTransportPair; the pad-bit case stamps the bit past Reserve; both death controls name their diagnostic via WireLogFatal (stderr echo); the RSS ledger reads /proc once and keeps its own running peak (GitHub run 35079459114) 2026-09-16 07:04:08 -04:00
swung0x48 d628d906d1 [Feat] (MG_Remote, P5): land the client role - BackendObject_Remote, the 71-slot emit table, the caps mirror and the verb barrier 2026-09-11 16:46:24 -04:00
swung0x48 f33e5d6090 [Fix] (MG_Remote, Transport, Client): SEG_STAGE is w1's encoder-local linear allocator and not a ring - drop the stage RingProducer and its power-of-two rounding, pin the stage cursor triple dead, require a wrap filler to carry kind kRingPadRecordKind as well as the flag it shares with kVarTail, and name every kRecBorrowSlot sighting 2026-09-11 15:41:56 -04:00
swung0x48 8024dab9f4 [Fix] (MG_Remote, Client): fold the null union payload into both handshake guards, close the server session on every Start failure that ran after Accept succeeded, and drain teardown against the producer's own published seq rather than a watermark the ring lets lag 2026-09-11 15:04:54 -04:00
swung0x48 558fb210dc [Feat] (MG_Remote, Client, Server): the Hello/Welcome/CapsSnapshot handshake and both sessions' construction and teardown - the ABI assertion runs in Accept before a record is decoded and is Fatal{AbiMismatch} rather than a downgrade, and the two doorbell accessors live on the session rather than on ITransport 2026-09-11 14:13:20 -04:00
swung0x48 a45c0a0e18 [Feat] (MG_Remote): land the P5 contract headers and their named-Fatal stubs so all seven parallel packages compile and link on day one 2026-09-11 13:05:05 -04:00