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.
36 KiB
P5 scout — backend installation, BackendObject_Remote, and the thread/EGL question
Tree: /home/swung/w7/pipe @ a29807cc (P4a landed). READ-ONLY survey; nothing built, nothing run.
All line numbers are from that commit. Where I am guessing I say "guess".
0. Headline for the brief writer
Three things, before anything else:
- The two function-pointer tables are dead code today.
gMGPipeScreen/gMGPipeContext(MobileGL/MG_Pipe/MGPipe.h:137-138) are defined, zero-initialised, and never written by anyone; the generated thunksMGP_*(MG_Pipe/generated/PipeThunks.inc:20-302) that read them are never called by anyone. The live P2..P4a boundary is a set of direct function calls fromMG_Impl/Pipe/*intoMGPipeApply*(MG_Pipe/PipeApply.cpp). So "installBackendObject_RemoteinInit.cpp" by itself redirects nothing: P5 must also decide how the ~50MGPipeApply*call sites get routed through a table. See §2. - The GL context is current on the app thread today, and moving it to
mgl-srv-applyis not a no-op:IsBackendContextCurrentOnThisThread()(DirectGLES.cpp:12002) gates 16 call sites inDirectGLES.cppand, viaCanTouchGLNow()(Managers.cpp:928), 19 more inManagers.cpp. Every one of them silently DEGRADES (returns null / "already signaled" / defers the upload) when called from a non-owner thread. Under inproc those calls arrive on the app thread unless P5 moves them across the wire first. See §3 — this is the risk. MOBILEGL_TRANSPORTdoes not exist in any form. NoMG_Config::Transport, no enum, no parse.grep -rn "MOBILEGL_TRANSPORT\|TransportKind\|Transport" MobileGL/Config.h MobileGL/ConfigLoader.cpp→ zero hits. Three of the P5 exit-gate artifacts (PersistentCoherentMapScenario, aTrianglescenario,DirectGLES.Split.*lanes) also do not exist yet. See §4, §6.
1. The installation path today
1.1 Startup chain
MobileGL::Initialize() — MobileGL/Init.cpp:101-138:
Init.cpp:107 MG_Util::Debug::InitFile()
Init.cpp:109 MG_ConfigLoader::Init() <- MOBILEGL_TRANSPORT would be parsed here
Init.cpp:114 MG_Util::PipeStats::Init()
Init.cpp:115 MG_State::Init()
Init.cpp:117 MG_Backend::Init() <- THE HOOK POINT
Init.cpp:119 MG_Impl::Init()
Init.cpp:121 glslang::InitializeProcess()
Initialize() is reached lazily from the first EGL entry point:
MG_Impl/EGLImpl/EGLImpl.cpp:35-38 (GetStateEnsureInitialized() → MobileGL::EnsureInitialized()).
1.2 MG_Backend::Init() — the exact hook
MobileGL/MG_Backend/Init.cpp:
48 void Init() {
49 MGLOG_D("Initializing MobileGL Backend...");
50
51 switch (MG_Config::ActiveBackendType) { <- INSERT THE #if HERE
52 case BackendType::DirectGLES:
53 pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>();
54 break;
55 case BackendType::DirectVulkan:
56 pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>();
57 break;
58 case BackendType::Unknown:
59 default:
61 pActiveBackendObject = nullptr;
62 }
63
64 Bool result = InitSpecificBackendLibs();
...
70 }
The single hook P5 is supposed to add goes at MobileGL/MG_Backend/Init.cpp:50 (immediately
before the switch at :51), matching docs/Disaggregated/ARCHITECTURE.md:29
("唯一 hook 点是 MG_Backend::Init()…MG_Config::Transport != Monolith 时装
MG_Remote::BackendObject_Remote,否则走今天的 switch"). ARCHITECTURE says P0's Init.cpp
does not contain it — confirmed, it does not.
Note the second half of the same file, which P5 also has to satisfy:
38 Bool InitSpecificBackendLibs() {
43 pActiveBackendObject->Initialize();
44 gBackendFunctionsTable = pActiveBackendObject->GetBackendFunctions();
45 return true;
46 }
gBackendFunctionsTable is populated from the backend object — so a BackendObject_Remote
whose GetBackendFunctions() returns an all-null table would turn 91 MG_Impl/GLImpl call sites
into null-pointer calls. (Count: grep -rn gBackendFunctionsTable MobileGL/ → 91 in
MG_Impl/GLImpl, 2 in MG_Impl/Pipe, plus tests.) This is a real P5 item that ROADMAP's P5 row
does not name.
1.3 The globals
MobileGL/GlobalObjects.cpp:23 UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
MobileGL/GlobalObjects.cpp:24 GlobalBackendFunctionsTable gBackendFunctionsTable;
MobileGL/MG_Backend/BackendObjects.h:16-17 the two extern declarations
MobileGL/Config.h:21 extern BackendType ActiveBackendType; (a `Transport` sibling would go here)
Frontend readers of pActiveBackendObject outside MG_Backend/ (the surface a role-shim must
cover) are few: MG_Impl/EGLImpl/EGLImpl.cpp (:40-46, :150, :263, :275, :284, :312, :326, :346,
:448), MG_State/GLState/Core.cpp:34, MG_IntegrationTest/Harness/BackendCapsPeek.cpp:17,29.
Everything else is tests.
1.4 The two tables — declared, generated, never installed
MG_Pipe/generated/PipeTables.inc:17 struct MGPipeScreen { 11 fn ptrs }
MG_Pipe/generated/PipeTables.inc:33 struct MGPipeContext { 60 fn ptrs }
MG_Pipe/generated/PipeTables.inc:94-96 kMGPipeScreenCallCount=11, kMGPipeContextCallCount=60, total 71
MG_Pipe/MGPipe.h:137-138 inline MGPipeScreen gMGPipeScreen{}; inline MGPipeContext gMGPipeContext{};
MG_Pipe/generated/PipeThunks.inc:20+ inline MGP_GetCaps(...) { gMGPipeScreen.GetCaps(...); } ... (71 thunks)
Generator: scripts/gen_pipe.py from MG_Pipe/PipeCalls.def. Consumer of the catalogue:
MG_Test/Pipe/PipeCatalogueTest.cpp.
Verified absence (WSL grep over the whole tree): the only occurrences of gMGPipeScreen /
gMGPipeContext are the two definitions in MGPipe.h and the 71 reads inside PipeThunks.inc.
No assignment, no InstallPipe-style function, no = {…} aggregate anywhere. And
grep -rn "MGP_[A-Z][A-Za-z]*(" MobileGL/ | grep -v generated/ returns nothing — the thunks
have no callers. (The only "install" named in the tree is
MGPipeInstallClientResourceCallbacks(), MG_Impl/Pipe/ResourceTracker.h:606, called from
MG_Impl/Pipe/PipeFill.cpp:628 — that is the callback direction, §2.3, not these tables.)
2. What the boundary actually is today, and what BackendObject_Remote must replace
2.1 The live path (P2..P4a)
app GL call
-> MG_Impl/GLImpl/* (validate)
-> MG_Impl/Pipe/{PipeFill.cpp, Tracker.h, CsoCache.h, TextureEmit.h, SamplerEmit.h,
FramebufferEmit.h, ProgramEmit.h, VertexInputEmit.h, ResourceTracker.h}
-> MGPipeApply*(...) DIRECT CALL, MG_Pipe/PipeApply.{h,cpp}
- writes MG_Pipe::gPipeInputs (defined MG_Backend/MGPipe/PipeInputs.h:706)
- dispatches object/resource work through MGPipeResourceOps (PipeApply.cpp:402, g_resourceOps)
-> backend reads gPipeInputs at draw time (MGB_CTX, ARCHITECTURE.md:344)
Representative MGPipeApply* call sites (all direct, not through a table):
MG_Impl/Pipe/PipeFill.cpp:645, 678, 691, 705, 727, 747, 761, 784, 799, 839, 1403, 1424, 1465, 1502, 1523, 2013, 2026, 2115, 2184; MG_Impl/Pipe/CsoCache.h:157, 178;
MG_Impl/Pipe/FramebufferEmit.h:459; MG_Impl/Pipe/VertexInputEmit.h:161, 181, 245, 270, 399.
Declarations: MG_Pipe/PipeApply.h:740-1052 (≈45 entries).
MG_Pipe/PipeApply.h:15-17 states the intent explicitly: "the in-process applier: the SERVER
half of the calls P2 emits. Under split this file is MG_Remote/Server/PipeApplier".
So the two readings P5 must choose between, and I cannot settle from the tree alone:
- (A) Retrofit the tables. Turn each
MGPipeApply*intoMGP_*(the generated thunk), and have monolithInitinstall a table of&MGPipeApply*while remoteInitinstalls a table of emitters. Cost: touching every call site listed above; benefit: exactly the shape ARCHITECTURE §1.2 describes, andMG_Testcan keep mocking by table substitution. - (B) Branch inside the applier. Leave the call sites alone and put the transport fork inside
PipeApply.cpp/ a thinPipeEmitter. Cheaper, but thengMGPipeScreen/gMGPipeContextremain dead and the "one hook inInit.cpp" claim (ARCHITECTURE.md:29) becomes false.
Evidence that would settle it: whether any P5 gate asserts on the tables (nothing in the tree
does today), and whether the "B 门符号" purity gate
(ARCHITECTURE.md:516, nm --undefined-only libMobileGLServer.so) can pass under (B) — under (B)
the server links PipeApply.cpp, which includes MG_Impl/Pipe headers only in the client
direction, so I think it still can (guess).
2.2 MGPipeResourceOps — the precedent for how a backend registers a table
MG_Pipe/PipeApply.h:85-102:
85 struct MGPipeResourceOps { Create, Respecify, SubData, SubDataResident, FlushRange,
Readback, Destroy, MapPersistent, UnmapPersistent }; (9 members)
101 void MGPipeSetResourceOps(const MGPipeResourceOps* ops);
102 const MGPipeResourceOps* MGPipeGetResourceOps();
Storage MG_Pipe/PipeApply.cpp:402 (g_resourceOps), setters at :1094-1095.
DirectGLES's table: MG_Backend/DirectGLES/Managers.cpp:2298-2308 (g_glesResourceOps),
registered/unregistered around RegisterBufferBackendOps() (DirectGLES.cpp:11933,
Managers.cpp:2584). DirectVulkan registers none — that is the "consumer gate" that makes
the four P4a families emit nothing on Magma (ARCHITECTURE.md:213-…, and
MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp:119, 784, 857).
This is the pattern P5 should copy for the server's op tables; it is also the thing that will
make a DirectVulkan split lane vacuous.
2.3 BackendObject — the interface BackendObject_Remote must implement
MobileGL/MG_Backend/BackendObject.h:567-644. Lifetime: owned by
UniquePtr<BackendObject>& pActiveBackendObject; constructed in MG_Backend/Init.cpp:53/56,
destroyed at MobileGL/Init.cpp:68 (pActiveBackendObject.reset()).
8 pure virtuals — every one must be answered by the remote object:
| line | member | what a remote must do |
|---|---|---|
| 572 | void Initialize() = 0 |
bring up transport + handshake (no GL) |
| 573 | Bool InitCapabilities() = 0 |
this is the MGPCaps snapshot round trip |
| 574 | Bool InitWindowSurface() = 0 |
ship the native token; ARCHITECTURE.md:559-563 |
| 590 | const RendererInfo& GetRendererInfo() const = 0 |
from the caps snapshot; must return a reference, so the remote has to own a RendererInfo |
| 591 | String GetBackendAPIVersionString() const = 0 |
from snapshot |
| 592 | const GlobalBackendFunctionsTable& GetBackendFunctions() const = 0 |
see §1.2 — 91 MG_Impl/GLImpl sites read the table this returns |
| 593 | const DynamicBackendParameters& GetDynamicParameters() const = 0 |
from snapshot; struct is BackendObject.h:297-…, ~70 scalars incl. MaxComputeWorkGroupCount[3] |
| 595 | BackendType GetBackendType() const = 0 |
must answer the server's backend, not "Remote" — many frontend branches switch on it (guess: returning a new enumerator would break them) |
9 EGL-lifecycle virtuals with base implementations (BackendObject.cpp:168-494), the
"EGL 生命周期 8 项" of ARCHITECTURE.md:19 (I count nine, not eight — ResizeEGLWindowSurface
is the likely uncounted one):
:576 InitializeEGLDisplay, :577 CreateEGLWindowSurface, :578 ResizeEGLWindowSurface,
:579 CreateEGLPbufferSurface, :580 MakeEGLCurrent, :581 SwapEGLBuffers,
:584 SetEGLSwapInterval, :585 ReleaseEGLSurface, :586 ReleaseEGLResources.
Plus 2 protected: :624 InitPbufferSurface, :625 OnEGLSurfaceReleased.
Also inherited, and remote-hostile, is the base class's own state
(BackendObject.h:627-638): m_eglStateMutex (recursive), m_formatCapabilities
(FormatCapabilityCache — a full per-target × per-format matrix, populated by GL probes in
BackendObject_DirectGLES.cpp:855-866), m_eglCurrentThreads, m_eglSurfaces.
GetFormatCapabilities() is non-virtual (BackendObject.h:589, impl
BackendObject.cpp:478), so a remote object must fill m_formatCapabilities from the caps
snapshot rather than override the accessor. Reader:
MG_Backend/DirectGLES/DirectGLES.cpp:12446 (ClampSamplesToBackendSupport) and
MG_IntegrationTest/Harness/BackendCapsPeek.cpp:29.
Two base behaviours P5 must not lose:
BackendObject::MakeEGLCurrentcallsInitCapabilities()on first make-current per surface (BackendObject.cpp:341-347) — i.e. the caps snapshot's natural timing is already there.ActivateEGLSurfaceclearsm_eglCurrentThreadsand resetsm_backendCapabilitiesInitialized(BackendObject.cpp:301-302).
3. THE THREAD QUESTION
3.1 Which thread calls the backend in monolith
The app's calling thread, always. There is no render thread.
Every MG_Impl/GLImpl entry point runs gBackendFunctionsTable.GL.* inline on the thread that
made the GL call (91 sites). grep -rn "std::thread\b" MobileGL/ outside tests finds no
thread creation at all except MG_Util/Async/ShaderCompilePool.cpp — and the compile pool is
explicitly forbidden from touching the backend
(MG_State/GLState/ProgramState/ShaderCompileTask.cpp:239,
ProgramLinkTask.cpp:564, ProgramSpirvTask.cpp:94: "no GL/EGL call, no
pActiveBackendObject read"). So today: one thread, the app's, does frontend + backend + the
driver call.
3.2 Which thread owns the EGL/GL context
DirectGLES keeps one process-global ES context, and its owner is a single global slot:
DirectGLES.cpp:11856-11864 comment: "The single backend ES context migrates between app threads
(FCL/pojav-style LWJGL hands the EGL context from JVM thread to JVM
thread). Ownership must live in ONE global slot..."
DirectGLES.cpp:11865 std::atomic<std::thread::id> g_backendContextOwnerThread{};
DirectGLES.cpp:11919-11955 Bool MakeCurrent() — the real eglMakeCurrent at :11925
DirectGLES.cpp:11931 g_backendContextOwnerThread.store(std::this_thread::get_id(), release)
DirectGLES.cpp:11958-11975 Bool ReleaseCurrent() — eglMakeCurrent(NO_SURFACE…) at :11964,
owner cleared at :11961 / :11973
DirectGLES.cpp:12002-12025 Bool IsBackendContextCurrentOnThisThread()
DirectGLES.cpp:11980-11996 the EGL verification stamp (g_eglVerifiedFrameSerial /
g_eglVerifiedContextGeneration) — re-verifies against
eglGetCurrentContext() once per (frame, context generation).
Comment: glvnd's eglGetCurrentContext measured at 16% of the render
thread, hence the stamp.
Is it ever current on the app thread today? Yes — it is only ever current on an app thread. The chain runs entirely on the caller:
app eglMakeCurrent
-> MG_Impl/EGLImpl/EGLImpl.cpp:240 MakeCurrent(...) [takes EGLOperationMutex, :241]
-> EGLImpl.cpp:284 backendObject->MakeEGLCurrent(dpy, draw, read, ctx)
-> MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:935 MakeEGLCurrent
-> BackendObject_DirectGLES.cpp:963 DirectGLES::MakeCurrent()
-> DirectGLES.cpp:11925 g_EGLFuncs.eglMakeCurrent(...) <- ON THE APP THREAD
-> DirectGLES.cpp:11931 owner := this thread
-> DirectGLES.cpp:11933-11953 BufferImpl::RegisterBufferBackendOps() + SIX cache
invalidations (broadcast memo, indexed buffer bindings,
pixel buffer bindings, framebuffer bindings + handle arm
memos, pack state, synced render state) + ApplyRequestedSwapInterval
Line 11933-11953 is what ARCHITECTURE.md:535 calls "MakeCurrent 的缓存失效风暴" — under
split it runs once at startup on mgl-srv-apply instead of on every app-thread migration.
BackendObject (the base) separately keeps a multi-thread map
UnorderedMap<std::thread::id, EGLCurrentState> m_eglCurrentThreads (BackendObject.h:637,
written BackendObject.cpp:350-355) — so the frontend's EGL bookkeeping already models "many
threads, one context", while DirectGLES's driver-side ownership is a single slot. These two are
consistent today only because the same thread does both.
3.3 What breaks if two threads touch it
Nothing crashes; everything silently degrades, which is worse. A non-owner thread makes
IsBackendContextCurrentOnThisThread() return false at DirectGLES.cpp:12006, and then:
DirectGLES.cpp — 16 guarded sites:
| line | function | degraded behaviour |
|---|---|---|
| 12034 | FenceSync |
returns nullptr → frontend substitutes an always-signaled sync |
| 12058 | ClientWaitSync |
returns GL_ALREADY_SIGNALED |
| 12069 | WaitSync (server-side wait) |
no-op |
| 12081 | DeleteSync |
leaks the native GLsync (wrapper only freed) |
| 12093 | GetSyncStatus |
reports signaled |
| 12197 | BeginTimerQuery-ish |
returns null handle |
| 12212 | end-query | skipped |
| 12221 | QueryCounterTimestamp |
null handle |
| 12242 | BeginCoreQuery |
null handle |
| 12257 | end core query | skipped |
| 12299 | glGetQueryObjectuiv read |
falls back |
| 12326 | query availability | falls back |
| 12373 | query delete | wrapper only |
| 12385 | GetGpuTimestampNs |
returns 0 |
| 12399 | WaitForFrameSerialCompleted |
returns false |
| 12428 | Present()'s frame fence |
no fence is created for the frame → the buffer pool's recycle watermark never advances |
Managers.cpp via CanTouchGLNow() (:928-930) — 19 sites: 1494 (persistent-map
acquire), 1589, 1610, 1632, 1681, 1747, 1793, 1919, 1939, 1986, 2038, 2087,
2170 (the second persistent-map tier), 2602 (ProcessDeferredBufferReleases), 3293,
3428. These are the buffer paths: a false answer means the write is deferred into
pendingRanges / pendingResidentWrites and applied later — correct only if a later call on
the owner thread drains them.
Concretely, the single most dangerous one for the P5 exit gate:
Managers.cpp:1494 and :2170 are the glBufferStorageEXT + glMapBufferRange persistent-map
acquisitions, and Ops_H_MapPersistentTracked (Managers.cpp:2306) is the op behind
MGPipeApplyMapPersistent (PipeApply.cpp:1918-1919, called from PipeFill.cpp:784). If the
app thread calls it while the apply thread owns the context, it declines and the
PersistentCoherentMapScenario gate is unreachable. ARCHITECTURE.md:502 already lists
MapPersistent (T1, once per storage definition) as one of the unavoidable blocking round trips
— i.e. the design says it must cross the wire, not be called locally.
3.4 What ARCHITECTURE.md says
ARCHITECTURE.md:535(§14 Present、线程与帧节奏): client: v1 不加线程, encoding happens on the GL thread and writes the ring directly (the frontend is already a per-context single-thread contract); foreign-thread sync/query reads are answered lock-free fromRingControl; the few that must emit takectrlMutexand go over CTRL as anAuxRequest(the SPSC ring allows no second producer);ShaderCompilePoolstays as-is on the client. server:mgl-srv-io(asio, framing,SCM_RIGHTS, doorbell, CTRL RPC),mgl-srv-apply(终身持有原生 context —g_backendContextOwnerThreadis written once, the MakeCurrent invalidation storm becomes a one-off at startup, the per-frame EGL re-verification is permanently true, off-thread degradation disappears), optionalmgl-srv-dec.ARCHITECTURE.md:536: bindmgl-srv-applyto a big core viaMOBILEGL_IPC_SERVER_AFFINITY(default auto), reusingShaderCompilePool's big-core probe.ARCHITECTURE.md:26(§1.3): inproc is the monolith's render thread — "把PrepareForDraw与驱动调用搬离 GL 线程,是本项目手上最大的单一 CPU 杠杆".ARCHITECTURE.md:581(§16):MOBILEGL_BUILD_DISAGGREGATED_INPROCdoes not exist yet; it impliesMOBILEGL_BUILD_DISAGGREGATEDand adds a role-isolation shim, because in inproc both roles live in one process. MGPipe cuts the process globals needing a role split from four (pGLContext,gBackendFunctionsTable,pActiveBackendObject,pDefaultFramebufferInfo) to two (the pipe tables andpActiveBackendObject).
The unresolved inproc problem this creates, and P5 owns it: pActiveBackendObject is ONE
global (GlobalObjects.cpp:23) and MG_Backend::Init() writes it once. In inproc the client
role needs BackendObject_Remote there and the server role needs the real
BackendObject_DirectGLES there, in the same address space. The Init.cpp hook as written in
ARCHITECTURE.md:29 produces only one of the two. Two readings:
- (i) the server role never reads
pActiveBackendObjectat all, so the server side can hold itsBackendObject_DirectGLESin a privateUniquePtrowned byServerLoop. Against this:DirectGLES.cpp:12446(ClampSamplesToBackendSupport) readspActiveBackendObjectfrom inside the backend, on the server side of the seam. - (ii) the role shim makes
pActiveBackendObjectrole-scoped (a thread-keyed accessor).ARCHITECTURE.md:581says both shims are "不在 GL 热路径的每次访问上", which reads like (ii). Evidence that would settle it: an inventory ofpActiveBackendObjectreads reachable from backend code (I found exactly one:DirectGLES.cpp:12446). If it is only that one, (i) wins cheaply by passing the format cache down instead.
3.5 What has to change for the app thread to give up the context
Today the app thread acquires the context at BackendObject_DirectGLES.cpp:963
(DirectGLES::MakeCurrent()). Under split, that call must not happen on the app thread at all:
BackendObject_Remote::MakeEGLCurrent should emit a make-current record and let the apply
thread run DirectGLES::MakeCurrent() once, then never release. The frontend already has the
serialisation point: EGLOperationMutex (EGLImpl.cpp:48-51), held by MakeCurrent (:241),
SwapBuffers (:163), DestroySurface (:305). ARCHITECTURE.md:213 says exactly this —
eglMakeCurrent is a flow-ownership transfer emitted under the existing EGLOperationMutex,
and ReleaseThread / SwapInterval should be made to take that lock too (they do not
today: EGLImpl.cpp:341 ReleaseThread and :435 SwapInterval take no lock).
Three call sites that today reach the driver on the app thread and must be re-pointed:
EGLImpl.cpp:284→MakeEGLCurrent→DirectGLES::MakeCurrent()EGLImpl.cpp:173→SwapEGLBuffers→BackendObject.cpp:396backendFunctions.Present()(noteBackendObject::SwapEGLBuffersfirst checksm_eglCurrentThreadsfor the calling thread,BackendObject.cpp:375-384— a remote object must override or relax this)EGLImpl.cpp:448→SetEGLSwapInterval→BackendObject.cpp:400-405
4. MOBILEGL_TRANSPORT parsing: none exists
MobileGL/Config.h: noTransportmember. The MGPipe block isConfig.h:319-398(PipePushat:349,PipeVerifyat:355,PipeStats,PipeLegacyMemos,PipeTexelRetainMb,PipeIndexMirrorMb,PipeStatsPeriod,PipeStatsFile).MobileGL/ConfigLoader.cpp: the MGPipe parse block is:248-282. No transport line. Helpers available to copy:QueryEnvVariable,QueryEnvFlag,QueryEnvUint32,QueryEnvUint64(:171documents the strict integer parse),QueryEnvQuirkOverride.ConfigLoader.cpp:34— everyMOBILEGL_/LIBGL_prefixed variable is accepted byInitializeAcceptedEnvVariablesby construction, so a new name needs no allow-list entry.- The enum-style precedent to copy is
InitBackendType()atConfigLoader.cpp:284-300, which writes the globalMG_Config::ActiveBackendType(declaredConfig.h:21, definedGlobalObjects.cpp:13) rather than aFeaturesmember.ARCHITECTURE.md:576requiresMG_Config::Transportto be aconstexpr MonolithwhenMOBILEGL_BUILD_DISAGGREGATED=OFF, so it cannot be a plainFeaturesfield — it needs the#ifshape. - Spec of the value:
ARCHITECTURE.md:583—MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>. - Related unset knobs (all P5+, none parsed today):
ARCHITECTURE.md:613(MOBILEGL_IPC_SERVER_PATH,_RING_MB(8),_STAGE_MB(32),_PRESENT_CREDIT(1),_SPIN_US(50),_POLL_ESCALATE(64),_PERSISTENT_BLOCK_KB(64),_ADOPT_TIER,_SHADOW_SHM,_INLINE_PAYLOADS,_SERVER_AFFINITY,_STRICT_ERRORS,_AUDIT,_TRACE,_ATTACH,_RESPAWN,_IDLE_EXIT_S(30)).
Build wiring that does exist
CMakeLists.txt:23option(MOBILEGL_BUILD_DISAGGREGATED ... OFF).CMakeLists.txt:453-466forces it OFF (normal variable, not cache) when3rdparty/flatbuffers/includeis missing.CMakeLists.txt:503-520appends only the 8MG_Remote/Transport/*.cppfiles — noClient/orServer/entries yet.CMakeLists.txt:572-574-DMOBILEGL_BUILD_DISAGGREGATED=1.CMakeLists.txt:605-609adds the flatbuffers include dir.CMakeLists.txt:484-499is theMOBILEGL_PIPE_PUSHsource list (PipeApply.cpp,PipeFill.cpp,SlotAllocator.cpp,MGPipeRenderStateSpans.cpp,PipeInputs.cpp,ProgramArtifactsCodec.cpp) — the P5 client/server files will need the same treatment.MOBILEGL_BUILD_DISAGGREGATED_INPROCdoes not appear anywhere in the tree (onlyARCHITECTURE.md:581, 595).MG_Remote/containsProtocol/andTransport/only. NoClient/, noServer/.
5. Teardown — what a two-role build has to sequence
5.1 The three nested teardown paths today
(a) MobileGL::DestroyImpl — MobileGL/Init.cpp:38-98, in order:
:50 MG_Util::PipeStats::Shutdown() final stats line + JSON dump
:56 ShaderCompilePool::Get().StopAndDrain() "the one cancellation path that waits"
:62 MG_Impl::GLImpl::DestroyAllSyncObjects() drains while gBackendFunctionsTable is still live
:67 MG_Impl::GLImpl::DestroyAllQueryObjects() ditto
:68 MG_Backend::pActiveBackendObject.reset() -> ~BackendObject_DirectGLES -> DestroyEGLContext()
:69 MG_State::pGLContext.reset()
:70 MG_State::pEGLContext.reset()
:71 pProxyTextureManager.reset()
:72 pDefaultFramebufferInfo.reset()
:78 glslang::FinalizeProcess() MUST be after :69 (comment :73-77)
:82 ShaderCompiler::ResetPrewarmLatch()
:87-90 translation-cache stats + clear
:91 MG_Backend::gBackendFunctionsTable = {}
(b) MG_Impl::EGLImpl::Terminate — EGLImpl.cpp:319-338:
state->TerminateDisplay(dpy) → backendObject->ReleaseEGLResources() (:326) → and only if
!HasAnyInitializedDisplay() && !HasAnyCurrentContext() → MobileGL::Destroy() (:333).
So (a) is normally reached through EGL, not at process exit.
(c) DirectGLES::DestroyEGLContext — DirectGLES.cpp:12472-12510:
:12473-12477 BufferImpl / XfbImpl / MultiDrawImpl / OnRestartSubstitutionContextDestroyed /
ScratchFBOImpl :: OnBackendContextDestroyed()
:12478 ReleasePackedWordScratchTexture()
:12479-12484 invalidate framebuffer / VAO / pack-state / broadcast-memo caches
:12486 ++g_backendContextGeneration
:12489 g_backendContextOwnerThread.store({})
:12491 ++g_syncContextGeneration (all outstanding fences become "signaled")
:12494-12496 abandon the 4-slot frame-fence ring, floor the completed watermark
:12498-12509 eglMakeCurrent(NO_SURFACE) ; eglDestroyContext ; eglDestroySurface ; eglTerminate
I checked the three OnBackendContextDestroyed bodies — Managers.cpp:2583-2596,
Managers.cpp:9849-9855, DirectGLES.cpp:1426-1433 — and none of them issues a GL call;
they only drop shadows, bump generations and unregister the ops table
(Managers.cpp:2584 UnregisterBufferBackendOps()). Good news: the destroy path itself is
thread-agnostic. (Managers.cpp:2602's CanTouchGLNow() belongs to
ProcessDeferredBufferReleases, a different function starting at :2598 — do not confuse
them; I nearly did.)
5.2 ARCHITECTURE's required split order, and where it disagrees with today
ARCHITECTURE.md:537:
拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool(先于
glslang::FinalizeProcess()与pGLContext析构)→MobileGL::Destroy()→ 释放 sync/query handle。
This is not today's order. Today DestroyAllSyncObjects / DestroyAllQueryObjects
(Init.cpp:62, 67) run before pActiveBackendObject.reset() (:68), and the comments at
:58-66 say that is deliberate — the registries are drained "while the backend function table
can still release the backend handles". ARCHITECTURE puts sync/query release after
MobileGL::Destroy(). Two readings:
- (i) ARCHITECTURE means "release the client-side handle objects last, after the server is
gone", because in split a sync handle is minted client-side (
ARCHITECTURE.md:500, "client 铸造 handle") and needs no backend call — so the ordering constraint that motivates today's:62/:67simply evaporates. - (ii) ARCHITECTURE is loose here and P5 should keep today's order.
Evidence that would settle it: whether
BackendObject_Remote'sGetBackendFunctions()table has realDeleteSync/DeleteQueryentries that must run before the transport closes. If it does, (ii); if syncs are client-minted, (i). I could not settle this from the tree because neither the remote table nor the client fence store exists yet.
5.3 Additional teardown hazards P5 must handle
EGLImpl.cpp:326ReleaseEGLResources()is called from the app thread, and for DirectGLES it runsDestroyEGLContext()(BackendObject_DirectGLES.cpp:981-985), which callseglDestroyContext/eglTerminate. In split those must execute onmgl-srv-apply(the context owner), soBackendObject_Remote::ReleaseEGLResourceshas to be a blocking request, not fire-and-forget — otherwiseMobileGL::Destroy()atEGLImpl.cpp:333proceeds while the server still holds the context.OnEGLSurfaceReleased→DestroyEGLContext(BackendObject_DirectGLES.cpp:986-989), reached fromBackendObject::ReleaseEGLSurface(BackendObject.cpp:446-463) and fromDestroyPendingEGLSurfaceIfUnused(:419-431). A surface destroyed while current is deferred (DestroyPending = true,:454) until the last thread releases it. Two roles make "the last thread" ambiguous.~BackendObject_DirectGLES()callsDestroyEGLContext()(BackendObject_DirectGLES.cpp:833-835), i.e.pActiveBackendObject.reset()atInit.cpp:68can destroy the native context on the app thread. Same problem as (1).Present()is the only frame-boundary drain (DirectGLES.cpp:12424-12470: fence poll,UboRingOnPresent/UnpackRingOnPresent/UploadRingOnPresent/TrimBufferPool,PipeStats::OnPresent()at:12467).ARCHITECTURE.md:531insistspresent↔eglSwapBuffersstays strictly 1:1 for exactly this reason. A shutdown that drops in-flight presents starves the retire path.PipeStats::Shutdown()atInit.cpp:50runs in the client role, but thepersistent-map-pushcounter the P5 gate demands is written where the push happens.MG_Util/Metrics/PipeStats.h:46-122listspersistent-map-pushas declared-but-unwired (ARCHITECTURE.md:617: "P0 未接线"). Whichever role owns the counter must flush before its half exits; with two roles there are twoPipeStatsinstances in inproc unless the shim scopes it (guess: it does not need scoping, since the counter is client-side).
6. Gate artifacts that do not exist yet (P5 must create them)
PersistentCoherentMapScenario— named inARCHITECTURE.md:500and in the ROADMAP P5 exit gate; no such file underMobileGL/MG_IntegrationTest/Scenarios/(86 scenarios listed;ClearThenReadPixelsScenario.cppexists, aTriangle*Scenario.cppdoes not).DirectGLES.Split.test prefix — the itest CMake has prefixesDirectGLES.(MG_IntegrationTest/CMakeLists.txt:739),DirectGLES.ForcedDepthStencilEmulation.(:786),DirectGLES.UnlocatedIoBlocks.(:817),DirectGLES.MallocPerturb.(:839),DirectGLES.AsyncOn./AsyncOff.(:868, :895),DirectGLES.OptimisticShaderStatus.(:921),DirectGLES.NoViewportArrayEmulation.(:945),DirectGLES.WidenedPacked16.(:983),DirectGLES.PointSizeDemotion.(:1000),DirectGLES.HandleRecycle.Handles./Legacy.(:1157, :1175). NoSplit., and noDirectGLES.Pipe.prefix either (ARCHITECTURE.md:518 names it as the target shape). The env-joining helper ismgl_itest_join_environment(MG_IntegrationTest/CMakeLists.txt:321); every list must append${MGL_ITEST_COMMON_ENV}(:1036, and the trap is restated atARCHITECTURE.md:584).- trace-replay
SPLITsuffix +-DTRACE_TRANSPORT=:add_trace_replay_testistools/trace_replay/CMakeLists.txt:311-383(therun_trace_case.cmakeinvocation is at:372);add_trace_replay_test_for_backendsat:384-387. Neither knows aboutSPLITorTRACE_TRANSPORTtoday. - Wire-layer tests that DO exist and are the model for new ones:
MobileGL/MG_Test/Wire/{RingTest,FramingTest,FdPassingTest,InProcessTransportTest,ProtocolSmokeTest}.cpp, registered only under the option (MobileGL/MG_Test/CMakeLists.txt:93-95). InProcessTransporttoday is a mutex + two condvars message queue (MG_Remote/Transport/InProcessTransport.cpp:41-51:std::mutex mutex,condition_variable cvfor messages andfdCvfor fd offers). It is the control plane only — it does not create a thread and it does not run the G3 codec. P5's "InProcessTransport runs the SAME G3 codec as spawn" means drivingPipeWire.inc(MG_Pipe/generated/PipeWire.inc, 939 lines) through it, plus aSEG_CMDring (MG_Remote/Transport/Ring.{h,cpp}) and aDoorbell. Note the header's own comment (InProcessTransport.h:13): "side is the monolith's own render thread, which is the single largest CPU…".
7. Quick reference — the file:line list P5 will keep reopening
| what | where |
|---|---|
| the hook | MobileGL/MG_Backend/Init.cpp:50 (before the switch at :51) |
| backend table populated from the object | MobileGL/MG_Backend/Init.cpp:43-44 |
| the two globals | MobileGL/GlobalObjects.cpp:23-24; decls MG_Backend/BackendObjects.h:16-17 |
BackendObject interface |
MobileGL/MG_Backend/BackendObject.h:567-644 (8 pure, 9 EGL, 2 protected) |
GlobalBackendFunctionsTable |
MobileGL/MG_Backend/BackendObject.h:293-299 |
DynamicBackendParameters |
MobileGL/MG_Backend/BackendObject.h:297-… |
| base EGL impl | MobileGL/MG_Backend/BackendObject.cpp:168-494 (MakeEGLCurrent :306, caps-on-first-current :341) |
| DirectGLES EGL overrides | MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:875-989 |
| context owner slot | MG_Backend/DirectGLES/DirectGLES.cpp:11865 |
| driver make/release current | DirectGLES.cpp:11919 / :11958 (real eglMakeCurrent :11925 / :11964) |
| owner predicate | DirectGLES.cpp:12002-12025 (+ stamp :11980-11996) |
| buffer-path owner gate | MG_Backend/DirectGLES/Managers.cpp:928-930 (19 users) |
| native context destroy | DirectGLES.cpp:12472-12510 |
| frontend EGL entry points | MG_Impl/EGLImpl/EGLImpl.cpp — MakeCurrent :240, SwapBuffers :162, Terminate :319, ReleaseThread :341, SwapInterval :435, DestroySurface :304, mutex :48 |
| library teardown | MobileGL/Init.cpp:38-98 |
| the two pipe tables | MG_Pipe/generated/PipeTables.inc:17 / :33; instances MG_Pipe/MGPipe.h:137-138 |
| the thunks nobody calls | MG_Pipe/generated/PipeThunks.inc:20-302 |
| the applier actually in use | MG_Pipe/PipeApply.h:740-1052, MG_Pipe/PipeApply.cpp |
| backend op-table precedent | MG_Pipe/PipeApply.h:85-102; PipeApply.cpp:402, 1094-1095; Managers.cpp:2298-2308 |
| server-side state block | MG_Backend/MGPipe/PipeInputs.h:706 (gPipeInputs) |
| config | MobileGL/Config.h:319-398; MobileGL/ConfigLoader.cpp:248-282; enum precedent :284-300 |
| CMake gating | CMakeLists.txt:23, 453-466, 484-499, 503-520, 572-574, 605-609 |
| design of record | docs/Disaggregated/ARCHITECTURE.md:29, 213, 500-502, 516-518, 531-537, 576-584, 595, 613 |
| ROADMAP P5 row | docs/Disaggregated/ROADMAP.md:21 |