Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
28 KiB
P5 scout — MG_Remote/Transport as it stands, and what P5 must add
Tree: /home/swung/w7/pipe @ a29807cc (P4a landed), branch feat/disaggregated. READ-ONLY survey;
nothing was built or run. All line numbers are from that commit.
TL;DR for the brief writer:
- The wire layer is complete and well tested, but it has ZERO production callers. Nothing in
libMobileGL.soconstructs aRingProducer/RingConsumer, sends a frame, or parses aCtrlEnvelope. The only includers ofRing.h,Framing.handprotocol_generated.houtsideTransport/itself are the five test binaries underMG_Test/Wire/. InProcessTransporttoday is a message-queue pair + two condvar doorbells. It touches neither the ring nor any codec. P5's gate "inproc runs the SAME G3 codec as spawn" cannot be satisfied by editingInProcessTransport.cpp; it needs a new ring-owning object aboveITransport(§3).- The G3 codec has no encoder at all and a decoder that is a bounds gate returning
false.MGPipeApplyWireRecord(MG_Pipe/generated/PipeWire.inc:708-928) validates every record's bounds and then reports "not applied", by its own comment "because no applier exists until P5 wiresMG_Remote/Server/PipeApplier.cpp".MGPWireRec_*has no writer anywhere in the tree.
1. File by file
MobileGL/MG_Remote/ has exactly two subdirectories: Protocol/ and Transport/. There is no
Client/ and no Server/ — ARCHITECTURE.md:573-576 marks both as [P5+], and SocketTransport
under Transport/ as [P6].
Sizes: Doorbell.cpp 259, Doorbell.h 268, FdPassing.cpp 323, FdPassing.h 67, Framing.h 208,
ITransport.h 130, InProcessTransport.cpp 293, InProcessTransport.h 77, Ring.cpp 306,
Ring.h 256, ShmSegment.cpp 49, ShmSegment.h 87, ShmSegmentPosix.cpp 191,
ShmSegmentWin32.cpp 162, WireLog.cpp 33, WireLog.h 38 — 2747 lines total.
Ring.h / Ring.cpp — fully implemented, no stub
RingControl(Ring.h:57-82): one 4096-byte page,static_asserts on size and alignment atRing.h:84-89plus lock-free asserts on the atomics. Cmd cursor tripleRing.h:59-61, stage tripleRing.h:64-66, the five watermarksRing.h:69-73, epoch / generation / two park flags / two event flagsRing.h:76-81.InitRingControlzeroes the page and setsserverEpoch = ringGeneration = 1so 0 always reads as "uninitialized" (Ring.cpp:44-50).RingCursorsValidcheckshead >= applied >= retiredand occupancy <= capacity (Ring.cpp:52-61).HardDrainRingrefuses unlesshead == applied == retired, else bumpsringGeneration(Ring.cpp:63-79).RingProducer: ctor validates power-of-two capacity in[kMinRingCapacity, kMaxRingCapacity]and a non-null base, otherwise latchesValid() == false(Ring.cpp:85-105).FreeBytesis measured against the retired tail (Ring.cpp:107-120).Reserve(Ring.cpp:122-181) refuses > half the ring (:128-149), returnsnullptr+MGLOG_Dwhen full (:157-162), emits the wrap pad automatically (:164-171).Publishis a single release store on the head (Ring.cpp:183-190).RingConsumer::Pop(Ring.cpp:218-276) acquire-loads the head, bounds-checks the header (8-alignment, >= header, <= available, no straddle), sets*outCorruptand returns false on violation, silently skipskRecPadrecords.PublishApplied/PublishRetired/PublishRetiredUpToatRing.cpp:278-304;PublishRetiredpublishes applied first so retired can never overtake it, andPublishRetiredUpToclamps to applied.- What is NOT there: every sequence watermark (
appliedSeq,submittedSeq,retiredSeq,completedFrameSerial,presentAckSerial) and both event flags (eventRingFull,eventDropped) are declared, zeroed byInitRingControl, and written by nothing in the tree. Grepping the whole repo for them finds onlyRing.h,Ring.cpp:48-49,77andRingTest.cpp:90-91(theDirectGLES.cpphits oncompletedFrameSerial/ringGenerationare Espryt's own unrelated locals,DirectGLES.cpp:11881,Managers.h:1051). So the whole credit/backpressure protocol of ARCHITECTURE.md §11.6 (byte credit+present credit,appliedSeqevery 64 records) is unwritten — P5 owns it. - Nothing constructs a ring over an
ShmSegmenteither: the only#includeofRing.houtsideRing.cppisRingTest.cpp:14, whose fixture puts the control page and the byte area on the stack / in astd::vector(RingTest.cpp:29-76).
Doorbell.h / Doorbell.cpp — fully implemented
Doorbellbase (Doorbell.h:86-184):Notify/Park/Reset/Dead, plus theWaittemplate (Doorbell.h:120-180) that is the actual spin-then-park protocol: test, spinspinUs, then loop { setparkedrelaxed →atomic_thread_fence(seq_cst)→ re-test →Park(chunk)→ clearparked→ re-test → bail onDead()→ bail on deadline }.NotifyIfParked(Doorbell.h:194-199): fence, then relaxed read of the flag, thenNotify. The documented precondition is that the watermark is already published — the file header (Doorbell.h:39-52) spells out why a seq_cst store/load pair would not be enough and why neither fence may be removed.CondVarDoorbell(Doorbell.cpp:35-100): counted signals (so a ring that arrives before the park is not lost),Kill()death latch set under the same mutexParktests it under (Doorbell.cpp:87-95),notify_allon kill.SocketDoorbell(Doorbell.cpp:108-255, POSIX only,Doorbell.h:229-266): one byte over anAF_UNIX SOCK_STREAMend,reventsinspected properly so a hung-up peer latchesm_deadinstead of spinning (Doorbell.cpp:190-217),Drain()level→edge conversion (:221-248).- Windows has no socket doorbell;
Doorbell.h:233-236says the Windows path will be an overlapped named pipe and is explicitly not part of this skeleton.
ShmSegment.h / .cpp / Posix / Win32
- POSIX is complete:
CreateusesASharedMemory_createon Android, rawSYS_memfd_createon desktop Linux,shm_open+ immediateshm_unlinkas fallback (ShmSegmentPosix.cpp:50-117);Adopttakes ownership of an SCM_RIGHTS fd and verifiesfstatsize >= the announced size before trusting the peer (ShmSegmentPosix.cpp:119-141);Map(readOnly)/Unmap/Close(:148-188).OpenNamedreturnsMOBILEGL_ERR_UNSUPPORTEDon POSIX (:143-146). - Win32 (
ShmSegmentWin32.cpp) is written but the file header says UNTESTED and explains why (ShmSegmentWin32.cpp:12-16). Both platform halves are listed unconditionally in the CMake source list so neither can rot (CMakeLists.txt:509-513). - Tested only indirectly, by
FdPassingTest.ChildSharesASegmentThatTheParentMapsAndVerifies.
FdPassing.h / FdPassing.cpp — fully implemented (POSIX), stubbed on Windows
Supported/CreateSocketPair/SendFd/ReceiveFd return MOBILEGL_ERR_UNSUPPORTED on non-POSIX
(FdPassing.cpp:37-45); the POSIX arm (:81-320) is a real sendmsg/recvmsg SCM_RIGHTS
implementation over a dedicated AF_UNIX SOCK_DGRAM socketpair (rationale FdPassing.h:19-26:
datagram boundaries keep ancillary data attached to its payload). kMaxSidebandBytes = 256
(FdPassing.h:38); a short sideband buffer is refused before the datagram is consumed, so no
descriptor is ever dropped.
Framing.h — header-only codec, implemented, zero production callers
[u32 'MGLF'][u32 len][payload], 64 MiB cap (Framing.h:46-48), compaction at 64 KiB
(Framing.h:53). AppendFrame (:56-77), FrameReader::Feed/HasMessage/TakeMessage/ParseHeader/ Consume (:85-199). Two contracts it exists to fix are documented at Framing.h:14-26: a bad magic
or oversized length latches the reader failed and returns MOBILEGL_ERR_PROTOCOL_MISMATCH
forever; a too-small destination returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the required size and
keeps the message.
The only non-test includer is InProcessTransport.cpp:12, and it uses the header solely for the
constant kMaxFramePayloadSize (InProcessTransport.cpp:117). Nothing frames a byte today — the
codec goes live with the socket transport in P6.
ITransport.h — interface only
See §4.
InProcessTransport.h / .cpp — implemented, but a message transport (see §3)
WireLog.h / WireLog.cpp
A single WireLogError(fmt, ...) (WireLog.h:31-36, WireLog.cpp:18-31) at ERROR level, so
Framing.h can log without pulling MG_Util/Debug/Log.h (and through it the 661-header frontend
umbrella) into a header of this layer. This is enforced by the purity gate's wire-header probe
(scripts/check_include_closure.py:136-144, forbidden closure member MobileGL/Includes.h).
P5 note: any new header under Transport/ reachable from ITransport.h inherits this rule.
Build wiring
MOBILEGL_BUILD_DISAGGREGATED option at CMakeLists.txt:23 (default OFF); missing flatbuffers
submodule shadows it OFF for that configure only (CMakeLists.txt:453-465); the eight .cpp files at
CMakeLists.txt:503-519; -DMOBILEGL_BUILD_DISAGGREGATED=1 at CMakeLists.txt:572-574; the
flatbuffers include path appended at CMakeLists.txt:605+. The Wire suite is registered only under the
option (MobileGL/MG_Test/CMakeLists.txt:105-106), and FdPassingTest is dropped on Windows
(MobileGL/MG_Test/Wire/CMakeLists.txt:14-17). CI asserts the pull build has no MG_Remote symbols
(.github/workflows/test.yml:1490-1499) and that protocol_generated.h is regenerable
(.github/workflows/test.yml:739,764).
2. What the five Wire suites pin (52 tests)
RingTest.cpp (16 tests) — fixture at :29-76 (stack control page + std::vector byte area).
:80ControlPageLayoutIsTheSharedContract— 4096/4096/8, post-init generations = 1, and that each contended group is 64-byte aligned and thatcmdHeadandcmdAppliedTailare on different cache lines (:103-113).:115non-power-of-two capacity rejected;:124capacity the 32-bitsizefield cannot describe rejected;:308a ring too small for the smallest record rejected.:145records round-trip in order;:171payload padded to 8;:181a record is never split across the wrap (pad emitted).:205full ring refuses and recovers once the consumer retires;:228/:234records larger than the ring / larger than half the ring refused;:255placeability does not depend on the head offset;:281a half-capacity record fits at every head offset. (These four are the ones that makenullptrunambiguous: withFreeBytes() >= totalit can only mean "too big, chunk".):322HardDrainRingbumps the generation only when quiesced.:341a corrupt header is refused with*outCorruptrather than dispatched.:373SpscProducerConsumerThreadsAgreeOnEveryRecord— 20 000 records, two real threads, consumer retires as it goes, ends withcmdRetiredTail == cmdHead.:446DoorbellHandoffWakesBothSidesOnEveryPublish— the publish-then-NotifyIfParkedorder in both directions, 2000 records, with a 5 sWaitdeadline so a lost wakeup is a red test, not a hang. Its comment (:439-445) is honest that it cannot prove the fence pairing, only the call order.
FramingTest.cpp (9) — :36 byte-at-a-time reassembly of two messages; :65 the magic's literal
on-wire bytes; :76 empty payload; :90 bad magic latches the reader dead; :111 oversized length
rejected before any allocation; :123 send-side cap; :132 buffer-too-small reports the size and
keeps the message; :160 take with nothing complete neither blocks nor corrupts; :182 the buffer
compacts instead of growing.
InProcessTransportTest.cpp (13) — :52 both directions; :71 order; :85 buffer-too-small keeps
the message; :103 poll (timeout 0) and finite timeout do not block forever; :121 shutdown drains
before it closes (queued messages survive the peer's Shutdown); :141 a blocked receiver wakes on
send and on shutdown; :166 the frame cap; :180 fd + sideband hand-over; :222 a frame wakeup
is not eaten by a waiter blocked on descriptors (the reason there are two condvars,
InProcessTransport.cpp:50-51); :279 doorbell wakes a parked waiter and NotifyIfParked only rings
when the flag is set; :306 an already-true condition never parks; :319 timeout; :344
ShutdownUnparksAWaiterWithNoDeadline — the design's steady state (spun, consumerParked=1,
kWaitForever), pinned with a 5 s bounded join so a regression fails red instead of wedging CI. This
is the test that makes CondVarDoorbell::Kill() load-bearing for P5's thread join.
FdPassingTest.cpp (9, POSIX) — :56 a real fork(): child creates a 64 KiB segment, fills it,
SendFds it with a sideband; parent first proves a short sideband buffer is refused without
consuming the datagram (:93-100), then adopts, maps read-only and compares every byte; :137
timeout; :152 bad arguments; :166 a segment with no sideband still carries its descriptor; :197
SocketDoorbell wakes a parked waiter; :227 times out and remembers an early wakeup; :260 stops
parking when the peer hangs up; :289 still delivers the last ring before a hangup.
ProtocolSmokeTest.cpp (5) — :43 Hello round-trips through the generated header after
VerifyCtrlEnvelopeBuffer + identifier check; :72 Welcome carries four SegmentRefs with the
canonical sizes (8/32/8 MiB + 256 KiB); :98 union tags are frozen wire values (Hello=1 …
LogLine=10, SegmentKind::Cmd=1, Adopt=6, LogLevel::Error=3, FatalCode::ProtocolCorruption=1);
:119 a truncated message fails verification; :128 the same bytes travel across
InProcessTransport unchanged.
Not pinned anywhere: the SEG_STAGE cursor set in a real two-role flow (the fixture can select
RingCursorSet::Stage but no test drives both rings together), any watermark/credit behaviour, any
ShmSegment-backed ring, Framing over a real byte pipe.
3. InProcessTransport today — the crux
It short-circuits. InProcessChannel (InProcessTransport.cpp:38-97) holds, per direction, a
std::mutex, two std::condition_variables (one for messages, one for fd offers — rationale at
:44-49), a std::deque<std::vector<std::uint8_t>> of whole messages, a std::deque<FdOffer>, and a
closed flag; plus two CondVarDoorbells (:95-96).
SendFrame(:111-135): argument checks, thekMaxFramePayloadSizecap (kept only so a payload legal here stays legal after the switch tospawn,:116-122), then onestd::vectorcopy onto the peer's deque andcv.notify_one(). NoAppendFrame, no magic, no length prefix, no ring.ReceiveFrame(:137-179): predicate wait withkWaitForever/wait_for, the buffer-too-small contract (:164-170, message stays queued), then amemcpyout andpop_front.PeekFrameSize(:181-185),ShareFd=::dup+ queued offer (:187-227),ReceiveFd(:229-281) mirrorsFdPassing::ReceiveFd's capacity rule exactly.Shutdown(:287) →InProcessChannel::Close(:74-92), which sets both directions closed,notify_alls both cvs, andKill()s both doorbells (:89-91) —Kill, notNotify, for the reason spelled out in the comment and pinned by the test atInProcessTransportTest.cpp:344.PeerDoorbell()/SelfDoorbell()(:289-291) hand out the two bells. This is the only coupling to the ring design: the transport owns the bells but not the ring.
So the P5 exit gate "InProcessTransport runs the SAME G3 encode/decode as spawn"
(ROADMAP.md:21; ARCHITECTURE.md:391 "InProcessTransport 走与 spawn 完全相同的 G3 编解码路径,
只在门铃/拷贝机制上不同") is about the hot path, not about this class. ITransport is explicitly
not on the hot path (ITransport.h:16-20); the G3 path is
emitter → RingProducer::Reserve → memcpy of MGPWireRec_X → Publish → NotifyIfParked → RingConsumer::Pop → bounds gate → applier. None of that chain exists above Ring.cpp.
Two readings of where the missing piece goes, and the evidence that settles it:
- (a) a new ring-owning session object in
Client/andServer/— the client end holds aRingProducer+ the peer bell, the server end aRingConsumer+ its own bell, over aRingControlthat lives in anShmSegmentfor spawn and can live in either anShmSegmentor plain heap for inproc; - (b)
InProcessTransportitself grows ring accessors andSocketTransportmirrors them in P6.
ARCHITECTURE.md:573-576 settles it for (a): the P5 file list adds nothing under Transport/ (the only
[P6] addition there is SocketTransport), while Client/{PipeEmitter,EmitTables,…} and
Server/{PipeApplier,ServerLoop,ReplyPool,EventRing,…} are both [P5+]. Guess: the segment
allocation and the Welcome/SegmentRef exchange belong to the same P5 object, since Welcome
already has exactly the four SegmentRef fields (protocol.fbs:68-76) and ProtocolSmokeTest.cpp:72
already pins their sizes.
A practical consequence worth putting in the brief: in inproc both roles are the same process, so
ShmSegment::Create + one Map is enough (no ShareFd) — but using ShmSegment anyway (rather than
new) is what keeps the spawn path from being a different code path, which is what the gate is asking
for.
4. What a second ITransport (P6 socket/spawn) must provide
Seven pure virtuals plus the role (ITransport.h:46-127):
| member | contract that binds an implementation |
|---|---|
SendFrame(MobileGLByteSpan) (:59) |
bytes is borrowed: copy it or finish the write before returning. > kMaxFramePayloadSize ⇒ MOBILEGL_ERR_INVALID_ARGUMENT. Caller serialises sends (:27-29). |
ReceiveFrame(span, outSize, timeoutMs) (:80-81) |
five codes: OK (consumed) / BUFFER_TOO_SMALL (message stays queued, *outSize = required) / TIMEOUT (timeoutMs == 0 is a poll, kWaitForever never times out) / TRANSPORT_CLOSED (peer gone and nothing buffered) / PROTOCOL_MISMATCH (latched dead, never recovers). |
PeekFrameSize() (:85) |
size of the next pending message or 0. |
ShareFd(fd, sideband) (:98) |
POSIX SCM_RIGHTS; Windows returns MOBILEGL_ERR_UNSUPPORTED, the section name travels in SegmentRef instead. Caller keeps ownership of fd. |
ReceiveFd(outFd, sideband, outSidebandSize, timeoutMs) (:107-108) |
*outFd is owned by the receiver; sideband must be >= FdPassing::kMaxSidebandBytes, checked before anything is read. |
Shutdown() (:122) |
idempotent and symmetric: tears down the whole connection, both ends stop sending, every waiter on either side unblocks, but already-queued messages stay readable. |
Role() (:124) |
Server / Client / InProcess (ITransport.h:40-44). |
Ready-made parts for P6: Framing.h (the reassembler, still callerless), FdPassing.cpp (the aux
socketpair), SocketDoorbell (Doorbell.cpp:108-255). Not ready: nothing turns a socket fd into an
ITransport, and there is no ServerMain (ROADMAP.md:22 assigns both to P6).
Design flag for the brief: the two doorbell accessors are not on ITransport — they are
non-virtual extras on the concrete InProcessTransport (InProcessTransport.h:64-68). If P5's
client/server code reaches for them through the concrete type, P6 has to either hoist them into
ITransport or re-plumb P5's call sites. Deciding that in P5 costs nothing; discovering it in P6
costs a package.
5. Threading and lifetime
- There is no thread creation anywhere in
MG_Remotetoday.grep -rn 'std::thread|pthread_create| std::jthread' MobileGL/MG_Remote/is empty; every thread in the Wire suites is the test's own. So P5 creates the first one. - ARCHITECTURE.md:535 is the design of record:
- client — no new thread in v1: the encode happens on the GL thread (the frontend is already a
per-context single-thread contract). Foreign-thread sync/query reads are answered lock-free from
RingControl; the few that must emit take actrlMutexand go out as anAuxRequeston the CTRL channel, because the SPSC ring admits no second producer.ShaderCompilePoolstays client-side. An optionalmgl-client-txis measurement-gated, not planned. - server —
mgl-srv-io(asio, framing,SCM_RIGHTS, doorbell, CTRL RPC) andmgl-srv-apply, which holds the native context for its whole life (g_backendContextOwnerThreadwritten once, so theMakeCurrentinvalidation storm becomes a one-off at startup and the off-thread downgrade disappears). Optionalmgl-srv-dec. - Placement: ARCHITECTURE.md:536 — reuse
ShaderCompilePool's big-core detection to pinmgl-srv-apply(MOBILEGL_IPC_SERVER_AFFINITY, default auto, resolved mask logged), and report per-thread CPU time each phase. ARCHITECTURE.md:308 is the related rule that a texture pull blocksmgl-srv-apply, never the application thread.
- client — no new thread in v1: the encode happens on the GL thread (the frontend is already a
per-context single-thread contract). Foreign-thread sync/query reads are answered lock-free from
- In
inprocthe apply side is a second thread inside the game process; ARCHITECTURE.md:26 frames that as the point ("同时就是 monolith 的渲染线程 … 手上最大的单一 CPU 杠杆"). - Lifetime mechanism already in place:
CondVarDoorbell::Kill()(Doorbell.h:211-221,Doorbell.cpp:87-95) exists precisely soShutdowncan join an apply thread parked withkWaitForever;InProcessChannel::Closecalls it (InProcessTransport.cpp:89-91) andInProcessTransportTest.cpp:344pins it. P5'sServerLoopshould park throughDoorbell::Wait(control.consumerParked, …, kWaitForever)and treatWait == false && Dead()as "shut down", which is exactly the shape the test asserts. - Role isolation: ARCHITECTURE.md:581 says
MOBILEGL_BUILD_DISAGGREGATED_INPROC(the role-isolation shim for the two remaining process globals, the pipe tables andpActiveBackendObject) does not exist yet and is listed as "计划(P5)" in the switch table at ARCHITECTURE.md:~600.InProcessTransport.h:19-23repeats that nothing in the current code depends on it. Whether P5 needs it on day one depends on whether the inproc server role ever readspGLContext— worth an explicit ruling in the brief.
6. The FlatBuffers control plane
MobileGL/MG_Remote/Protocol/protocol.fbs (235 lines) + generated/protocol_generated.h (58 866
bytes, committed) + mg_protocol_base.h (120 lines, pure C).
| message | lines | purpose |
|---|---|---|
SegmentKind |
:36-44 |
Cmd=1 Stage=2 Reply=3 Event=4 Shadow=5 Adopt=6 |
SegmentRef |
:48-53 |
{id, kind, sizeBytes, name}; the fd never travels in the message (SCM_RIGHTS on POSIX, name on Windows) |
Hello |
:59-66 |
{abiMajor, abiMinor, buildFingerprint, backendType, pid, configBlob} |
Welcome |
:68-76 |
{abi, serverPid} + the four SegmentRefs |
CapsSnapshot |
:86-96 |
the P5 MGPCaps snapshot's transport form: three byte blobs (dynamicParameters, rendererInfo, formatCaps) + extensions, apiVersion, the two 3-entry compute limits, tableSlotMask, prefersCpuXfbPrimitiveAccounting. Comment says it replaces the 40 pActiveBackendObject-> sites and 89 caps reads (cf. ARCHITECTURE.md:116) |
DefaultFramebufferInfo |
:98-104 |
width/height/color/depth/stencil |
SurfaceOpKind / WindowKind / SurfaceOp / SurfaceReply |
:110-148 |
the EGL/surface RPC: init display, create window/pbuffer, resize, release, make/release current; reply carries EGL version + DefaultFramebufferInfo |
ResyncRequest / ResyncDone |
:157-161 |
sent after the client sees serverEpoch move |
AuxRequestKind / AuxRequest |
:163-176 |
FenceClientWait, QueryResult, ScalarGet — the foreign-thread path that may not take the SPSC ring |
FatalCode / Fatal |
:178-191 |
ProtocolCorruption RingOverrun SegmentMismatch DeviceLost ServerCrashed AbiMismatch |
LogLevel / LogLine |
:195-206 |
<= Warn lossy, >= Error lossless + rate limited |
CtrlMsg union / CtrlEnvelope |
:217-232 |
root type, file_identifier "MGLC" (:234-235); union tags are wire values, append only |
- Nothing uses any of it. The only
#includeofprotocol_generated.hin the tree isProtocolSmokeTest.cpp:13. Regeneration isscripts/gen_protocol.py, never in the default build graph; CI'sflatc-checkregenerates and diffs (.github/workflows/test.yml:739,764). mg_protocol_base.hgives P5 its result codes (:97-114, incl.MOBILEGL_ERR_BUFFER_TOO_SMALL=11andMOBILEGL_ERR_SHM_EXHAUSTED=8), the two spans,MobileGLShmRegion(:86-91), the id typedefs, ABI 1.0 macros (:51-56) and the structSize-first versioning rules (:27-34).
Two gaps P5 should decide about explicitly:
FatalCodehas noUnmigratedPipeInput. P5's exit gate "a read of an unmigrated field =Fatal{UnmigratedPipeInput}" is already served by a local fatal, not a wire message:MGPipeInputPoisonFatalForVerb=MGLOG_F+std::abort()(MG_Backend/MGPipe/PipeInputs.h:18-47), and — importantly —MOBILEGL_PIPE_POISONis auto-armed byMOBILEGL_BUILD_DISAGGREGATED(PipeInputs.h:22-25), so every P5 build has it on with no extra knob. The harness already greps for it (tools/trace_replay/run_trace_case.cmake:174,183). So the gate is about exercising the poison under split, not about implementing it. If the server role must instead report it to the client, that is a newFatalCodetag — an append, perprotocol.fbs:212-216.- There is no credit/ack message, by design: ARCHITECTURE.md §11.6 puts present-credit and
byte-credit entirely in
RingControl(presentAckSerial,appliedSeqevery 64 records, reverse doorbell), which is the unwritten half flagged in §1.
7. Shortest list of what P5 must add in this subject area
- A ring-owning session/channel object pair (Client + Server), constructing
RingControl+RingProducer/RingConsumeroverShmSegmentfor bothinprocandspawn, with the doorbellsInProcessTransportalready hands out — this is what makes the gate's "same G3 codec" true. - The G3 encoder (
MGPWireRec_*writers; nothing writes one today) and the real body ofMGPipeApplyWireRecord, whose skeleton comment (PipeWire.inc:703-707) names P5 andMG_Remote/Server/PipeApplier.cppas the owner. The bounds gate and theFatalon a bad opcode (PipeWire.inc:689-700, 924-927) are already there and already fatal. - The watermark/credit protocol: somebody must actually write
appliedSeq,submittedSeq,retiredSeq,completedFrameSerial,presentAckSerial,eventRingFull,eventDropped. - Handshake use of the control plane (
Hello/Welcome/CapsSnapshot/SurfaceOp) — today zero callers. MOBILEGL_TRANSPORTparsing inConfigLoader.cpp(ARCHITECTURE.md:583; the string does not appear in any.cppin the tree — only in docs andInProcessTransport.h:19), plus theMOBILEGL_IPC_*env family listed at ARCHITECTURE.md:615, of which onlyMOBILEGL_IPC_SPIN_US's default is coded (Doorbell.h:67,kDefaultSpinUs = 50) and nothing reads the env var.- Test wiring:
add_trace_replay_testneeds theSPLITsuffix and-DTRACE_TRANSPORT=, and the ctestENVIRONMENTtrap is already documented (ARCHITECTURE.md:584 — property replaces rather than appends,;must be escaped, usemgl_itest_join_environment).
8. Cautions
- Do not add an include of
MG_Util/Debug/Log.h(or anything reachingMobileGL/Includes.h) to a new header underTransport/reachable fromITransport.h— the purity gate fails (scripts/check_include_closure.py:136-144); useWireLog.h. - Keep
MG_Remotesymbols out of the pull build or.github/workflows/test.yml:1492-1499goes red. CtrlMsg/SegmentKind/FatalCode/LogLevelnumbering is pinned byProtocolSmokeTest.cpp:98-117: append only, and if that test needs editing the change was a wire break.RingProducer::Reserveis non-blocking by contract; a P5 emitter that waits must wait onproducerParked+ the reverse doorbell, and must never interpretnullptrwithFreeBytes() >= totalas "wait" (Ring.h:176-190).