[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).
This commit is contained in:
2026-09-21 12:23:41 -04:00
parent 5239ae0c19
commit 7e244b3775
17 changed files with 443 additions and 47 deletions
+34
View File
@@ -226,6 +226,26 @@ namespace MobileGL::MG_Backend {
// the client half here.
Bool InitSplitRoles() {
using namespace MobileGL::MG_Remote;
// SPAWN: THE SERVER ROLE IS NOT IN THIS PROCESS. Steps 1 and 2 -
// the backend and the two CallMask halves - happen in the server,
// which ran InitServerRoleForSpawn before it accepted us. Running
// them here as well would build a second BackendObject in the one
// process that must not have one, and would publish a capability
// mask computed from the WRONG backend's function table.
if (MG_Config::Transport == MG_Config::TransportMode::Spawn) {
const MobileGLResult started = Client::ClientSessionInstance().StartSpawned();
if (started != MOBILEGL_OK) {
MGLOG_E("MG_Remote: the spawn session failed to start (rc=%d); MobileGL will "
"NOT fall back to monolith - a lane named split that ran monolith is "
"the one failure this phase is built to make impossible",
static_cast<int>(started));
return false;
}
pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
return true;
}
if (!InitServerRoleCommon()) {
return false;
}
@@ -308,6 +328,19 @@ namespace MobileGL::MG_Backend {
MGLOG_W("Failed to initialize MobileGL backend libraries for the remote object");
return;
}
// P6: BOTH CHECKS BELOW ARE STATEMENTS ABOUT THE SERVER'S PROCESS, and under
// spawn that is not this one. Their own wording says so - "a mask is a
// statement about THIS BACKEND", "the table the SERVER'S BACKEND registered at
// step 1" - and in a spawn client there is no backend and never was a table, so
// MGPipeGetResourceOps() is null by construction and the first check fires on a
// correct session. It did, on the first spawn retrace ever run.
//
// The checks are NOT weakened: the server runs the step-2 one itself, inside
// InitServerRoleForSpawn, against its own freshly registered table - which is the
// process where the question has an answer. What is lost under spawn is the
// step-5 re-run's ability to catch a table REPLACED between accept and here, and
// that is a question about one address space; it cannot be asked across two.
if (MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// m-6, re-worded per review v2 N-8. The honesty cross-check runs a SECOND time, now
// that step 4's pActiveBackendObject (the client's BackendObject_Remote) exists and
// its Initialize() has run inside InitSpecificBackendLibs. What the re-run CAN catch
@@ -329,6 +362,7 @@ namespace MobileGL::MG_Backend {
static_cast<const void*>(g_resourceOpsAtStep2));
std::abort();
}
}
LogBackendInfo();
return;
}