mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
fe27380ab82dbdef8403f1b00be6601e347ed887
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fe27380ab8 | [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 | ||
|
|
52ffc6a7f2 | [WIP] (MG_Remote): implement P6.5 TCP and stream transport | ||
|
|
bf3e6f8ad3 |
[MG_Util, MG_Remote, tools] (P6): the wire counters and the spawn server's stats latch
Gate 8's last two numbers (SEG_STAGE bytes/frame, records/frame post- chunking) had no producer at all: the summary line's byte classes are backend-to-driver bytes and maxrec is a max, not a per-frame. Two push- guarded counters answer them at their single choke points - ByteClass:: StageSegmentBytes at PipeWireCodec's StageAllocate (the only allocation behind StageBytes, so every buffer walk, texture slab and CSO archive lands there and emitters cannot double-count) and CallClass::WireRecords on EncodeRecord's committed path (post-chunking an emitter no longer knows how many records its one call produced). A staged-blob histogram rides along. G1 holds: symbols 0/0/0/0, .text byte-identical, pull build carries no MG_Remote symbols. The spawn server never counted anything: PipeStats::Init() is a step of MobileGL::Initialize(), which ServerMain never reaches, so every Enabled() site in that process - including ServerLoop's own ServerWaits/ServerParks publish - stayed false and the server log carried no summary line at all (a zero that means 'another process' printed in the shape of one that means 'never waited' is the milder misreading; the line was simply absent). ServerMain now arms and closes PipeStats at its role boundary - after MG_ConfigLoader::Init (Init latches from the parsed config), after the server-role latch (the log sink's role), and before ::_exit(0). Red-once: 0 server summary lines before, 29 after, SSIM 1.0 unchanged. MOBILEGL_PIPE_STATS_FILE now derives per role through the log sink's exported RoleLogPath rule - run.client.json / run.server.json, the bare name resolves to no file - because both roles reach FormatJson() and ofstream trunc made the second writer replace the first. G1 caught three separate leaks on the way (a string temporary in the monolith body, one resized Shutdown() from unequal #if arms, two rewritten MGLOG_W literals in .rodata); the pull arm is statement-for-statement the original. Device tooling for the paired A/B lands with it: pin_device.sh gains a measured 2f7cbe2e entry (this board clamps the GPU at 1050 MHz - thermal_pwrlevel writes 0 and reads back 1, max_gpuclk is read-only; absolute times are not comparable with the 1100 MHz-era rows), and tools/device_bench/p6/ carries the session collector and renderer beside run_android_retrace_local.py's MOBILEGL_TRACE_PACKAGE override. Unit 2367/2367 (PipeStats 25/25), spawn integration 102/102. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
e156ea5f7a |
[MG_Remote] (P6/sm): two MobileGL processes, talking
The first time this project has had a second process. fork + execve of a real server image, the control socket surviving the exec, bytes crossing a process boundary, and the child exiting on EOF with nothing left behind. mobilegl_server_main EXISTS NOW. a6 found ARCHITECTURE.md:488-496 had described it for three phases while the symbol was nowhere in the tree, and the only MobileGLServer target was the P0 spike stub, which links no MobileGL library at all. The spike keeps its evidence and its name becomes MobileGLServerSpike; the real image links the library, so an undefined symbol is a build error rather than a dlsym failure in a child whose stderr is /dev/null. WHAT sm OWNS AND WHAT IT LEAVES: the process, the inherited descriptors, the transport and the exit discipline. Not the session, not the applier, not the four segments - wiring those from ServerMain before lk has a seam to wire them THROUGH is how call sites get written twice. The loop is a read/answer/exit-on- EOF stand-in whose SHAPE is the part that matters: one reader, EOF ends the process, and no timeout is ever allowed to stand in for the death fact (§5.4). ANTI-RECURSION IS TWO CATCHES, and neither is ARCHITECTURE.md:488's. That rule says the child hard-sets Transport to Monolith; a6 measured that 226 live lines test `Transport != Monolith` to select the SERVER arm, 148 in MG_Backend, so obeying it flips the whole backend to frontend glue in the one process with no frontend. Instead: (a) MOBILEGL_IPC_DIAL=no, structural and separate from role, and (b) MOBILEGL_TRANSPORT and every MOBILEGL_IPC_* scrubbed from the child's envp. The child refuses with 64 for (a) and 65 for (b) so the two safeties are distinguishable - two that cannot be told apart are one. TWO BUGS WORTH RECORDING, both found by the test rather than by review: dup2(n, n) IS A NO-OP THAT DOES NOT CLEAR CLOSE-ON-EXEC. FdPassing sets SOCK_CLOEXEC on the aux pair, so the child depends on dup2 to clear it - but in a gtest process a fresh socketpair lands on exactly fds 3..8, so a source fd often IS its own target. The no-op left CLOEXEC set and the following close() then shut the descriptor we had just "installed". The child came up with no socket and the parent saw TRANSPORT_CLOSED on its first send. Now spelled out rather than written cleverly. THE CHILD'S DIAGNOSTICS WERE INVISIBLE. WireLogError reaches the file sink only - Defines.h builds the logger with the console sink off - so a child that refused to start said so into a file the parent does not know the path of. ARCHITECTURE.md §15.2 warns about exactly this on Android. Both refusal paths now also write stderr directly, and both carry a distinct exit code the parent can name without reading anything. TESTS: six cases, 6/6. Each exists because something would otherwise pass silently - the reply carries the child's own getpid() so a test that talked to itself cannot pass (ID-124's lesson at the smallest scale); the process tree is COUNTED before, during and after, not promised; the child is proven not to exit while merely idle before it is proven to exit on EOF; an unresolvable image is a named refusal that spawns nothing (S1); a surviving client knob is caught by name (S3); and a descriptor crosses execve by SCM_RIGHTS, which is the mechanism the four segments will ride on and is now exercised before it is needed. Also records the asio decision in the contract: declined for P6 because spawn needs five platform mechanisms and asio covers one - its own socketpair returns operation_not_supported on Windows, it has no descriptor passing, segment delivery is POSIX-only by design, and there is no fork. Revisit at P6.5, where asio::generic::stream_protocol carries a raw family/type/protocol triple and therefore gives AF_VSOCK from one implementation. Gates: unit suite 2346, up 6, same two pre-existing environmental failures. G1 pull 0 added / 0 removed, .text size 10822147 unchanged, and zero spawn, server or SocketTransport symbols in the pull build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |