a6 is the read-only audit P6-SPAWN-PLAN puts first. Seven static items, each
produced by one auditor and re-resolved line by line by one adversarial
verifier; 198 rows, 65 of them contested, and only the surviving ones carry
into the report. Item 5 - can the server link without MG_Impl - is the one
experiment, and it is answered.
The headline is that P6's transport half is smaller than the plan assumed
(fc and fs really did land) and its process/role half is much larger. Four
statements in P6-SPAWN-PLAN / P6-CONTRACT-DRAFT are false at this head:
- `spawn` is not an observable state at all. ConfigLoader.cpp:339-346
recognises it and then writes Transport = Monolith, so "the child was
forced to monolith" and "the parent fell back" are the same value and
that named refusal cannot falsify itself. Meanwhile 226 live lines test
`Transport != Monolith` to select the SERVER arm, 148 of them in
MG_Backend - so ARCHITECTURE.md:488's rule, implemented literally, flips
the whole backend to frontend glue in the one process with no frontend.
- `cp` is not transport-only: the InitCapabilities REQUEST has no wire form,
and the codec refuses to encode the kind a live client still calls.
- the device-lost latch is not implemented; it was booked as carriage.
- the server cannot link without MG_Impl: 184 symbols, 177 of them real
function calls, owed 101 to P7 / 14 to P3b-P4b / 60 shared / 6 to P6.
There is also no module target to link, so the question could not have
been answered by editing a link line.
Four more blockers the earlier review missed: the apply thread aborts in
ConsumerDoorbell before popping its first record (contract 3.9's ruling, which
lk should retire rather than amend); neither side of the control plane is
wired to a socket; the control-reply wait has no deadline, so a frozen peer
wedges the GL thread inside eglMakeCurrent; and SurfaceReply cannot carry a
MobileGLResult, so a client cannot tell a dead server from a legitimate
refusal.
The abort census makes dl's funnel a redesign rather than a rename: 92 sites,
30 distinct family words against a 7-value wire enum, and three unbounded
peer-driven allocations with no try/catch anywhere under MG_Remote.
P6-ENDSTATE-REVIEW.md is the re-examination of P6 against the gfxstream-shaped
end state that produced a6's three added items. a6 overturned four of its own
claims - the MG_State eight's shape, the abort count, m_windowHandle's
injection points and a predicate name collision - and those are corrected in
place rather than left to be rediscovered.
Docs only. No code, no gate, no behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**小结**:I opened every site below at b3b9c2ee. The honest headline is that the *byte-carrying* families are closed: `MGHostSpan::Ptr` is written non-null nowhere in the tree (the single producer, WireTables.cpp:603-607, writes nullptr explicitly), every `MGPBlobRef` that could hold a host address is caught at the ENCODER by CheckBlobIsHonest whose BlobSlotsFor table covers all eleven blobref members in MGPipeTypes.h, the only blobref resolve outside the codec (ResourceTracker.h:598-618) is a bounds-checked read of the client's own SEG_EVENT mapping, the reverse producers copy payload into SEG_EVENT rather than shipping pointers, MapPersistent declines by ruling, and the sixteen FATAL PipeInputs rows make every server read of a frontend pointer a named abort. What is NOT closed is *identity*: EGL display/surface/context still cross as bit-cast host pointers (ServerLoop.cpp:838-846 `TokenFromHandle`/`HandleFromToken`, consumed at :908-946), `nativeToken` is an `ANativeWindow*`/HWND as an integer, and the wire path that would refuse them (`ServerApplyWireSurfaceOp`) has **no production caller at all** - the live inproc path never reaches it. The single thing c6 most needs from this item is the answer to its own PENDING question: forcing `MG_Config::Transport = Monolith` in the child is **not** safe, because 151 sites in MG_Backend alone (208 tree-wide) select their server-role arm on `Transport != Monolith`, and flipping them selects frontend glue that does not exist in a second process. The anti-recursion control must be a separate flag from the role selector. Second-most important: `MGPipeServerBlockNoteIdentity()` is gated on the `MOBILEGL_IPC_ROLE_SPLIT_STATE` knob (default false), so under spawn with the knob off the server block's `ContextIdentity()` stays nullptr and DirectGLES' fb-slot memo reads that as a cache HIT against its own nullptr initialiser - an unnamed null-slot crash where the comment at PipeInputs.h:878-884 says a named one was intended. Third: the server cannot link without MG_Impl today - `PipeInputs::IsLive()` and the seven F-class forwarders live in MG_Impl/Pipe/PipeFill.cpp:2135-2230, and both role-refusal helpers live in MG_Impl/Pipe/SlotAllocator.cpp:29-57, so dropping MG_Impl from the server image drops the guards with it.
| A1-1 | already-handled | MGHostSpan::Ptr - the one pointer field on the wire. Verified: NO site in the tree ever assigns it a non-null value. | MobileGL/MG_Pipe/MGPipeHostSpan.h:32 (struct MGHostSpan); MobileGL/MG_Remote/Client/WireTables.cpp:603-607 (Wire_Escape_SetProgramBindings - the only producer, writes `entry.Name.Ptr = nullptr` explicitly); MobileGL/MG_Impl/Pipe/ProgramEmit.h:336 (`MGPProgramStorageOverride entry{};` - the monolith producer value-initialises it) | Nothing. There is no non-null Ptr to break. I grepped every `.Ptr =` and `Ptr = ` in the tree outside MG_Test: the only assignment is the nullptr one. | CheckHostSpanIsHonest arm 1, PipeWireCodec.cpp:527-533, Fatal{ProtocolCorruption,"host-span"} on Ptr != nullptr - run at | a6 | |
| A1-2 | already-handled | Host spans ARE emitted today - exactly one family. The audit asked; the answer is yes, not zero. MGPProgramStorageOverride::Name is an MGHostSpan and set_program_bindings (opcode 80) emits one per storage-block override. | MobileGL/MG_Pipe/MGPipeTypes.h:1062-1067 (`MGHostSpan Name; // the block name, NUL included, staged whole`); MobileGL/MG_Pipe/PipeCalls.def:340 (`SetProgramBindings ... kVarTail/kHostSpan`); MobileGL/MG_Remote/Client/WireTables.cpp:589-607 (stages each name, fills the span); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1946-1975 (decoder resolves each) | Nothing in this row by itself - the span is already fully wire-shaped. It matters because it falsifies the standing claim that P5 emits zero host spans, so every host-span guard is LIVE code with real traffic rather than latent P8 code. | CheckHostSpanIsHonest(span, segments) at the decoder, PipeWireCodec.cpp:1954; plus the NUL-termination check at :1955-19 | a6 | |
| A1-3 | p6-must-handle | The ENCODER's blanket host-span honesty pass does not cover set_program_bindings' spans. It requires `layout.TailCount == 2 && layout.SecondTailIsHostSpans`; set_program_bindings has THREE tails and SecondTailIsHostSpans is deliberately false, so the only check on those spans runs at the decoder. | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1144-1155 (the encoder pass and its two conditions); :717-722 (`// NOT SecondTailIsHostSpans: the spans are MEMBERS of the third tail's elements`); :1946-1954 (the decoder-side per-element check that does fire) | A malformed override span is refused in the SERVER process. The client sees the peer die and takes the device-lost latch; the operator gets a server-side Fatal naming a record the client produced, and the producing call stack is in the other process. It is a diagnosis loss, not a correctness loss. | partial - decoder-only (PipeWireCodec.cpp:1954). No encoder-side arm. | c6 | |
| A1-4 | no-action | kDrawHasUserIndices is set by NO emitter in the tree, so MGPipeHostBytes - the only consumer of gMGPipeSegmentResolver - is unreachable today. | MobileGL/MG_Pipe/MGPipeHostSpan.h:50-56 (MGPipeHostBytes); MobileGL/MG_Remote/Server/PipeApplier.cpp:743 (the ONE caller); MobileGL/MG_Remote/Client/EmitTables.cpp:1916-1918 and :411 ("NO kDrawHasUserIndices: the reduced path draws from a VBO"); MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h:51 (clears the bit) | Nothing, and it would still be correct if it did fire: gMGPipeSegmentResolver is installed in the SERVER process by ServerSession::Accept, so a span resolved there names the server's own mapping of SEG_STAGE. This is the family that is genuinely finished. | CheckHostSpanIsHonest arm 4 (segment-range, PipeWireCodec.cpp:543-563) plus PipeApplier.cpp:745-750's null-resolve Fatal | P8 | |
| A1-5 | already-handled | MGPBlobRef carrying a raw host address: the monolith emitters still write one into Blob.Offset, and the split emitters overwrite it. The encoder catches any that is not overwritten. | MobileGL/MG_Impl/Pipe/ProgramEmit.h:227 (`Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData())`), :434, :438; MobileGL/MG_Impl/Pipe/TextureEmit.h:1458-1459, :1511, :1529; MobileGL/MG_Impl/Pipe/VertexInputEmit.h:409-411 (Seg=None with a NON-ZERO Size); guard at MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:488-500 and the encoder pass at :1122-1127 | Nothing new. The failure mode is identical to inproc and already aborts on the producing side, which is the process that can name the call site. | CheckBlobIsHonest, PipeWireCodec.cpp:459-512, called for every blob slot of every emitted record at :1123-1127 | a6 | **line-does-not-resolve** — Re-cite (a) as VertexInputEmit.h:410-412 and (b) as PipeWireCodec.cpp:456-475 for the two named arms (and note :495-505 as a THIRD arm - 'a content blob that names any segment but SEG_STAGE' - which t |
| A1-6 | later-stage | BlobSlotsFor is the coverage of the encoder's blob guard. I enumerated every MGPBlobRef member in MGPipeTypes.h (11: MGPCaps x2, MGPRenderStateDesc, MGPDynamicState, MGPVertexElements, MGPSamplerDesc, MGPProgramDesc Spirv[6]+Reflection, MGPGlobalConstants, MGPResidualValueState, MGPSubData, MGPStorageBlockBinding) against the table. Coverage is complete. | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:303-330 (BlobSlotsFor); MobileGL/MG_Pipe/MGPipeTypes.h:174,175,476,501,531,541,636,1096,1195,1292,1797 (every blobref member) | Nothing today. The standing hazard is append-only growth: a new record type with a blobref that nobody adds to this switch gets no encoder check, and under spawn the resulting Fatal lands in the peer process. | none for the table itself - there is no static_assert or generator tie between MGPipeTypes.h's blob members and this swi | P7 | |
| A1-7 | already-handled | MGPBlobRef resolved to an address OUTSIDE the codec: exactly one site in the tree - the client's writeback consumer resolving kSegEvent through ClientSession::Segments(). | MobileGL/MG_Impl/Pipe/ResourceTracker.h:598-618 (the kSegEvent arm, `session->Segments().Resolve(kSegEvent, bytes.Offset, bytes.Size)`); producer side MobileGL/MG_Remote/Client/ClientSession.cpp:285-300 (`blob.Offset = events.OffsetInSegment(bytes)`); the Seg==None arm's transport refusal at ResourceTracker.h:583-592 | Nothing, PROVIDED the spawn client maps SEG_EVENT and installs it at ClientSession.cpp:663-665 exactly as AttachInProcess does today. If the adopt path maps only three of the four segments, Resolve answers nullptr and the site aborts by name - which is the correct failure, not a silent one. | ResourceTracker.h:583-592 (Seg==None under transport) and :607-616 (out-of-segment run); both Fatal{ProtocolCorruption} | so | **line-does-not-resolve** — Re-cite as ClientSession.cpp:654-662 (all four Installs; SEG_EVENT at :660-662). Verdict already-handled / phase so unchanged and correct - the precondition 'spawn's adopt must map all four slots' is |
| A1-8 | already-handled | gMGPipeSegmentResolver - one plain non-atomic inline variable per process, installed by the SERVER role only, via a file-static table pointer. | MobileGL/MG_Pipe/MGPipeHostSpan.h:47; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:348-355 (g_processResolverTable + ProcessResolverThunk), :428-449 (Install/Uninstall); installed at MobileGL/MG_Remote/Server/ServerSession.cpp:566, uninstalled at :665; the client explicitly does NOT install (ClientSession.cpp:645-665) | Nothing - it becomes trivially correct. In the server process the server installs it; in the client process it stays nullptr and nothing calls MGPipeHostBytes. The double-install Fatal loses its reason to exist but stays harmless. Note for c6: PipeCatalogueTest.cpp:1122 asserts it is nullptr, which stays true in a client process and stays false in a server one - the test's home moves. | WireProtocolFatal at PipeWireCodec.cpp:434-438 | a6 | |
| A1-9 | already-handled | gMGPipeCallbacks - ten function pointers at namespace scope. Four are installed by ServerSession::Accept as PRODUCERS; the client's installer is monolith-gated; six entries have no caller at all. | MobileGL/MG_Pipe/MGPipeCallbacks.h:61 (the global); MobileGL/MG_Remote/Server/ServerSession.cpp:575-580 (installs OnGlError/OnBufferWriteback/OnGpuWritten/OnSurfaceChanged), :666-678 (releases only its own); MobileGL/MG_Impl/Pipe/ResourceTracker.h:679-695 (client installer) gated at MobileGL/MG_Impl/Pipe/PipeFill.cpp:866-869 | Nothing. Each process gets its own copy of the inline variable: the server's holds its four producers, the client's stays all-null and its consumers are called BY NAME from DrainEventRing (ClientSession.cpp:266-300), never through the table. What the producers hand the wire is a COPY: ServerOnBufferWriteback memcpys the backend's mapped bytes into SEG_EVENT (ServerSession.cpp:299-303) and ServerOnSurfaceChanged copies MGPSurfaceInfo field by field (:332-341). No host pointer reaches the ring. | ServerSession.cpp:356-366 and ResourceTracker.h:680-690, both Fatal{RoleViolation}; plus MGPipeAnnounceBufferGpuWritten' | a6 | |
| A1-10 | blocks-p6 | EGLDisplay / EGLSurface / EGLContext cross as bit-cast host pointers. TokenFromHandle is `reinterpret_cast<uintptr_t>` and HandleFromToken is its inverse; the frame's display/surface/readSurface/context are those bits. | MobileGL/MG_Remote/Server/ServerLoop.cpp:838-846 (TokenFromHandle / HandleFromToken); posted at :1063-1066, :1078-1082, :1107-1119, :1123-1128, :1139-1143; consumed at :908-909, :915, :923, :928, :943-946; field declarations at MobileGL/MG_Remote/Server/SurfaceControlFrame.h:72-76 | HandleFromToken<EGLDisplay>(frame.display) hands the server's eglInitialize a pointer from the CLIENT process's EGL. There is no check: it is a reinterpret_cast into a driver call. Best case EGL_BAD_DISPLAY, worst case an address that happens to be mapped in the server. Every one of the five ops above is affected. | none. The only range check on the frame is WindowBackendFromFrameValue (ServerLoop.cpp:872-882), which validates the bac | cp | **line-does-not-resolve** — Re-cite as ServerLoop.cpp:839-848 (the pair), SurfaceControlFrame.h:75-78 (the four token fields), ServerLoop.cpp:864-873 (WindowBackendFromFrameValue). Verdict blocks-p6 / phase cp unchanged and corr |
| A1-11 | blocks-p6 | SurfaceControlFrame::nativeToken is `ANativeWindow*` / HWND / XID as an integer. | MobileGL/MG_Remote/Server/SurfaceControlFrame.h:78 (`Uint64 nativeToken = 0; // HWND / XID / ANativeWindow* as an integer`); packed at MobileGL/MG_Remote/Server/ServerLoop.cpp:864-869 (PackWindowHandle -> TokenFromHandle(handle.Handle)); unpacked at :876-881 into a WindowHandle handed to CreateEGLWindowSurface (:915) and SetWindowHandle; source type MobileGL/MG_Backend/BackendObject.h:579-584 (`void* Handle`) | A window surface created from a client-process ANativeWindow* in the server process. The named refusal for exactly this exists - SurfaceWireError::AndroidNativeWindowArrived -> Fatal{UnmigratedSurface,"AndroidNativeWindow@P12"} - but it lives on the wire decode path, which nothing calls (see A1-12). On the live frame path there is no refusal at all. | exists but unreachable: MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:157-166 (decode) and :197-215 (the Fatal) | cp | **line-does-not-resolve** — Re-cite as: SurfaceControlFrame.h:83 (the field); ServerLoop.cpp:853-858 (PackWindowHandle, the pack at :855); :875-882 (UnpackWindowHandle, the unpack at :878); :915 (handed to CreateEGLWindowSurface |
| A1-12 | p6-must-handle | The wire surface-op codec - the half that carries every named process-local refusal - has NO production caller. `ServerApplyWireSurfaceOp` and `EncodeSurfaceOpFrame` are referenced only by their own header and by MG_Test. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:116 (EncodeSurfaceOpFrame), :197 (ServerApplyWireSurfaceOp); the live path is MobileGL/MG_Remote/Server/ServerLoop.cpp:658-670 (RunSurfaceControlFrame) -> :886-960 (ApplySurfaceControlFrame), which never touches the codec | Not a break - an unarmed gate. Every rule-G refusal in this family (AndroidNativeWindow@P12, MetalLayer@P12, InprocOnlyOpOnTheWire, UnknownWindowKind) can only go red once cp routes the frame through the socket. A spawn lane that kept the by-value path would satisfy the exit gate while never once running the code that refuses a process-local window. | n/a - the guards ARE the unreached code | cp | |
| A1-13 | blocks-p6 | The client attaches to the server's shared memory by holding a C++ reference to the server's SessionSegments object, not by receiving descriptors. | MobileGL/MG_Remote/Client/ClientSession.cpp:533 (`Server::ServerSession& server = Server::ServerSessionInstance();`), :575-586 (Welcome cross-check reads `server.Shm().AnnouncedSize(slot)` directly), :599-601 (`m_shm.AttachInProcess(server.Shm(), MemoryRole::Client)`), :704 (`Server::ServerLoopInstance().Start(server)`); MobileGL/MG_Remote/Transport/SessionRings.h:160 (AttachInProcess signature); MobileGL/MG_Remote/Transport/ShmSegment.cpp:311-352 (dup + Adopt + Map off `owner.m_segments[index]->Fd()`) | There is no `server` object in the client process. Three separate things break: the Welcome geometry cross-check has nothing to compare against, AttachInProcess has no owner, and ServerLoopInstance().Start(server) is starting the other process's apply thread. SessionSegments has no constructor that takes received descriptors - only Create and AttachInProcess. | none - it is a compile-time coupling, so it fails to build rather than failing silently. That is the safe direction. | so | **line-does-not-resolve** — Fix :533 -> :534 and add the teardown sites to the row's `site` column: ClientSession.cpp:758, :802, :814 and MG_Backend/Init.cpp:255. ARCHITECTURE.md:504 already rules that under spawn the client's d |
| A1-14 | blocks-p6 | Segment descriptors never cross: ShareFd / ReceiveFd have no production caller, SegmentRef carries no fd, and on Android the segment name is not openable. | MobileGL/MG_Remote/Transport/ITransport.h:98,107 (the two virtuals); MobileGL/MG_Remote/Transport/InProcessTransport.cpp:187-226 (a real dup-based implementation, unused); MobileGL/MG_Remote/Protocol/protocol.fbs:48-53 (SegmentRef = id/kind/sizeBytes/name - no fd field); MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:62-65 (ASharedMemory_create - the label is not a resolvable name); MobileGL/MG_Remote/Transport/ShmSegment.h:22-24 says so | On Android the four segments cannot reach the child at all. ASharedMemory gives an anonymous fd; SegmentRef::name is a debug label; there is no shm_open path to it. SCM_RIGHTS is the only route and nothing calls it outside MG_Test. | none - the primitives are built and unit-tested (FdPassing.cpp:173, MG_Test/Wire/FdPassingTest.cpp) but unwired | so | |
| A1-15 | p6-must-handle | No pid crosses. Hello::pid is hardcoded 0 and Welcome::serverPid is filled from the CLIENT's Hello, so it is always 0. | MobileGL/MG_Remote/Client/ClientSession.cpp:519-521 (`/*pid=*/0u`); MobileGL/MG_Remote/Server/ServerSession.cpp:532-534 (`CreateWelcome(builder, abiMajor, abiMinor, static_cast<Uint32>(hello->pid()), ...)` - the fourth argument is serverPid); MobileGL/MG_Remote/Protocol/protocol.fbs:64 (Hello.pid), :80 (Welcome.serverPid) | Two things. (a) P6-SPAWN-PLAN §6.5's arm-proof gate wants each test's private log to carry the child pid plus `transport=spawn`; serverPid is the natural carrier and it currently echoes the peer's value, so a spawn lane could log `serverPid=0` and still look armed. (b) The process-tree gate (§6.4) counts children externally; a serverPid the client can assert against is the in-band version of the same check. This is a pid crossing that SHOULD exist and does not, rather than one that must be removed. | none - no consumer, so a wrong value is unobservable | sm | |
| A1-16 | blocks-p6 | ClientSession owns BOTH transport ends as concrete `unique_ptr<InProcessTransport>` and reads the doorbell pair off the concrete type. The server's mirror of the same access is guarded; the client's is not. | MobileGL/MG_Remote/Client/ClientSession.h:357-358 (both members typed InProcessTransport); MobileGL/MG_Remote/Client/ClientSession.cpp:622-623 and :632 (`m_clientTransport->PeerDoorbell()` / `SelfDoorbell()`); the guarded server equivalent at MobileGL/MG_Remote/Server/ServerSession.cpp:709-740 (role check then Fatal{UnmigratedVerb,"ServerSession::ConsumerDoorbell"} for any non-InProcess role) | A SocketTransport cannot be stored in either member, and StartOverTransportPair's signature takes two InProcessTransport unique_ptrs. The client also cannot legitimately own the server end at all. It is a compile-time break, which is the honest direction - but note the asymmetry: ServerSession already has the named-Fatal shape for an unmigrated transport role and ClientSession has no equivalent. | partial - ClientSession.cpp:482-490 refuses non-inproc modes by name at Start, which is what keeps this from being reach | so | **line-does-not-resolve** — Re-cite the members as ClientSession.h:355-356. Verdict blocks-p6 / phase so unchanged and correct. |
| A1-17 | blocks-p6 | InitSplitRoles builds BOTH roles in the one process: the server's private BackendObject, its CallMask, its CapsSnapshot source, then the client session, then the client's BackendObject_Remote. | MobileGL/MG_Backend/Init.cpp:148-224 (the whole function); specifically :155-158 (`loop.CreateBackend(...)` - the server's backend, on the app thread), :162 (`ServerSessionInstance()`), :209-210 (`ClientSessionInstance().Start(...)`), :221 (`pActiveBackendObject = MakeUnique<BackendObject_Remote>()`) | Steps 1 and 2 - backend creation, the consumer-mask honesty cross-check (AssertConsumerMaskIsHonest, :117-138) and the capability-bit derivation off `loop.Backend()->GetBackendFunctions()` (:183-205) - all have to run in the CHILD, after exec, before the Welcome. `g_resourceOpsAtStep2` (:145) is a pointer compared against MGPipeGetResourceOps() later, which only means anything inside one process. | the caller refuses to continue on false rather than falling back to monolith (the named refusal at :210-216) - so a fail | sm | **line-does-not-resolve** — Re-cite as: Init.cpp:151-230 (InitSplitRoles); :157-158 (loop.CreateBackend on the app thread); :119-139 (AssertConsumerMaskIsHonest); :146 (g_resourceOpsAtStep2); :163 (ServerSessionInstance); :184-2 |
| A1-18 | already-handled | gPipeInputs' O-class members are raw frontend pointers and SharedPtrs. All nine reachable getters are classified FATAL, and a server read under a stamped verb aborts by name. | MobileGL/MG_Backend/MGPipe/PipeInputs.h:808-818 (SharedPtr<VertexArrayObject>, BindingSlot<BufferObject>*[15], BindingSlotRange1D<BufferObject>*[15], BindingSlot<FramebufferObject>*[2], ImageTextureBinding*, three SharedPtr<ProgramObject>, TextureUnit*); classification at MobileGL/MG_Pipe/FieldOwnership.def:28-43; enforcement at MobileGL/MG_Backend/MGPipe/PipeInputs.h:96-105 (MGP_INPUT_CHECK) and MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:382-414 (MGPipeInputUnfreshRead) | Nothing, and the failure direction is right: in a server process gPipeInputs is filled by nobody, every field is withdrawn, and any MGB_CTX read of any class aborts with the field and verb named. This is the family P5f genuinely closed. The exception is A1-20. | MGP_INPUT_CHECK / MGPipeInputUnfreshRead, Fatal{UnmigratedPipeInput} and Fatal{RoleViolation}; plus MGPipeStickyForwardP | a6 | |
| A1-19 | blocks-p6 | The dual-block selection and the server block's identity are gated on a KNOB (MOBILEGL_IPC_ROLE_SPLIT_STATE, default false), not on the transport. Under spawn the rehearsal is not optional - it is the reality - and the knob-off path has no server block identity. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (`MGPipeRoleSplitActive() = Ipc.RoleSplitState && Transport != Monolith`), :297-299 (MGPipeClientInputs), :314-315 (`MGPipeServerBlockNoteIdentity` returns early when the knob is off); default at MobileGL/Config.h:550 (`Bool RoleSplitState = false`); the hazard is written out at MobileGL/MG_Backend/MGPipe/PipeInputs.h:876-884 | With the knob off in a server process, nothing ever calls SetIdentity on gPipeInputs, so ContextIdentity() stays nullptr for the life of the process. DirectGLES' fb-slot memo compares identity FIRST against a nullptr-initialised static, reads a HIT, and returns `*g_fbSlotCache[target]` - a null FbBindingSlot* - WITHOUT running MGP_INPUT_CHECK, because the cache-hit path bypasses the accessor entirely. That is an unnamed null dereference in exactly the place PipeInputs.h:880-884 says a named abort was intended. It is reachable whenever MOBILEGL_PIPE_LEGACY_MEMOS is on (CMakeLists.txt:43 - ON by default) and kMGPipeSubsystemEsprytSlots (bit 5) is clear. | none on this path - the memo hit is the bypass. The Fatal it should produce lives behind a knob that spawn cannot assume | c6 | |
| A1-20 | p6-must-handle | The fb-slot memo caches raw FbBindingSlot* pointers into the frontend GLContext and is keyed on MGB_CTX_IDENTITY - a raw pointer compare with no generation. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:199-213 (GetFramebufferBindingSlotChecked: `const void* ctx = MGB_CTX_IDENTITY; if (ctx != g_fbSlotCacheContext) { ... }`), statics at :177-178; the push-arm bypass at :195-198 (`if (EsprytSlotTablesEnabled()) return MGB_CTX->GetFramebufferBindingSlot(target);`) | Two ways, both only on the legacy arm. (a) A1-19's null identity makes the very first call a false hit. (b) Even with a correct identity token, the cached pointers address the CLIENT's GLContext, which does not exist here. The comment at :180-186 already records that this path bypasses the per-verb poison stamp at all five call sites. | structural only: EsprytSlotTablesEnabled() (SlotTables.h:150-153) routes past it, and ResolveEsprytSlotTablesArm Fatals | c6 | |
| A1-21 | p6-must-handle | The frontend-keyed twin registry and slot allocator are guarded, but the predicate is `ServerLoop::OnApplyThread()` rather than "this is the server process". | MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-43 (MGPipeRefuseAllocatorFromApplyThread) and :45-57 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread), both `if (!ServerLoop::OnApplyThread()) return;`; call sites at MobileGL/MG_Backend/DirectGLES/SlotTables.h:281, :398, :410, :476, :505 and MobileGL/MG_Backend/DirectGLES/Managers.h:341, :406, :430, :436; the allocator's own three at SlotAllocator.cpp:217, :230, :239 plus the singleton accessor at :329 | They stop coinciding. Server-process code that is NOT on the apply thread - backend bring-up before Start, and any RunSurfaceControlFrame dispatch that takes the non-apply-thread branch (ServerLoop.cpp:669-670) - passes the guard. MGPipeSlots() in the server process is then a freshly constructed, EMPTY allocator (SlotAllocator.cpp:338-339): FindByLifetimeId answers the null handle and Acquire mints a slot the client knows nothing about. Both are silent wrong answers where the inproc build gives a named abort. | Fatal{RoleViolation,"MGPipeSlots"} - correct message, predicate too narrow for two processes | c6 | |
| A1-22 | blocks-p6 | The server cannot link without MG_Impl today. PipeInputs' out-of-line definitions and BOTH role-refusal helpers live in MG_Impl translation units. This is the direct answer to P6-SPAWN-PLAN §5 item 5 and P6-CONTRACT-DRAFT §3.3. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141 (PipeInputs::IsLive - what MGB_CTX_LIVE expands to, read at 20+ MG_Backend sites), :2160-2230 (the seven F-class forwarders: GetBufferBindingPointCount, GetProgramObject, GetTextureObject, HasOpenTransformFeedbackSpan, InvalidateCompileEnv, ValidateProgramName, RecordError); MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-57 (both refusal helpers) and :324-340 (MGPipeSlots); the claim it contradicts is at MobileGL/MG_Backend/MGPipe/PipeInputs.h:281-284 and docs/Disaggregated/ARCHITECTURE.md:516 (§16 lists PipeInputs.{h,cpp} under MG_Backend/MGPipe/) | A server image that drops MG_Impl loses PipeInputs::IsLive (an undefined symbol at 20+ MGB_CTX_LIVE sites) and, worse, loses MGPipeRefuseAllocatorFromApplyThread and MGPipeRefuseFrontendKeyedRegistryFromApplyThread - so the guards of A1-21 vanish along with the code they guard. The realistic P6 answer is that the server links the whole library and the separation is enforced at run time, not at link time; a6 should say so rather than leave the claim standing. | none; check_include_closure.py does not probe MG_Backend headers (SlotTables.h:80-82 says so) | sm | |
| A1-23 | blocks-p6 | MG_Config::Transport is a process global that 208 sites switch on to pick the server-role arm, 151 of them in MG_Backend. Forcing it to Monolith in the child - which ARCHITECTURE.md §15.1 instructs - selects frontend glue. | MobileGL/Config.h:564 (the idiom named); representative server-arm selectors: MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:30 and :49 (the two role refusals), MobileGL/MG_Pipe/PipeApply.cpp:2286-2291 (R-6's MapPersistent decline), MobileGL/MG_Impl/Pipe/PipeFill.cpp:2136-2138 (PipeInputs::IsLive -> MGPipeServerContextIsLive), MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:293 (MGPipeRoleSplitActive), MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:19 (`const Bool server = Transport != Monolith`), MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2212 and :2257 (the XFB key selector), MobileGL/MG_Impl/Pipe/ResourceTracker.h:585 and :704 | If the child sets Transport = Monolith to stop recursive spawning, all 151 MG_Backend selectors flip to the monolith arm: the applier stops declining MapPersistent, IsLive stops being server-owned and calls LiveContext() (a client GLContext that is not there), ContextEpoch stops treating itself as the server, and both role refusals in SlotAllocator.cpp return early - the guards go quiet in exactly the process that needs them. If instead the child leaves Transport = Spawn, ConfigLoader/Init could try to become a client again. The two controls are one variable today and must become two. | none - the recursion fuse and the role selector are the same field | c6 | **overstated** — Replace the count with: 179 `Transport != ...Monolith` comparisons tree-wide (MG_Backend 138, MG_Impl 25, MG_Pipe 6, MG_State 4, MG_Remote 4), plus 45 further `TransportMode::Monolith` mentions of whi |
| A1-24 | no-action | Server-side addresses used as map keys that never cross - verified clean, recorded so c6 does not re-open them. | MobileGL/MG_Remote/Server/StagedTextureStore.h:214-217 (KeyForTwinAddress: a server-side twin address, tagged so it cannot alias the handle key space minted at :207-210); MobileGL/MG_Remote/Server/ServerLoop.h:161-163 (CurrentThreadKey via __builtin_thread_pointer, apply-thread identity only); MobileGL/MG_Pipe/PipeApply.cpp:1677 (`BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId` - a CLIENT-minted opaque integer that arrives in the set_context_values record, used as a server map key at DirectGLES.cpp:2007, :2213, :2258) | Nothing. None of these leaves the server process, and the XFB identity is the shape P5f fs chose precisely because a lifetime id is an opaque integer rather than an address. This is a family where the honest answer is "nothing left". | KeyForTwinAddress's top-bit tag (`Uint64{1} << 63` on the handle key) makes the two key spaces non-aliasing by construct | a6 | |
| A1-25 | already-handled | MGPipeApplyMapPersistent's host-pointer return is declined by ruling under any transport, on both the monolith applier path and the codec path. | MobileGL/MG_Pipe/PipeApply.cpp:2256-2302, specifically :2285-2292 (`if (Transport != Monolith && AdoptTierIsEmulate()) return nullptr;`); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1616-1638 (the codec arm never calls the applier and posts kStatusDeclined) | Nothing. The pointer that would be meaningless across a process boundary is never produced. Note for c6: the record payload is a bare MGPHandleOnly and carries neither size nor seedBytes, so the phase that lands a real map_persistent needs an append-only payload change before it can even call the applier (:1618-1622). | the two decline arms above; AdoptTierIsEmulate aborts by name at T0/T1 rather than returning | P11 | |
**复审另行指出的遗漏**:
- MobileGL/MG_Remote/Client/WireTables.cpp:87 - `Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }`. This is A1-21's exact defect (an apply-thread predicate standing in for 'this process is the server') on a far larger and more consequential surface: it is the selector between the WIRE EMITTER and the MONOLITH ADAPTER for every routed row. Consulted at WireTables.cpp:156 (MGP_WIRE_SIMPLE), :166 (MGP_WIRE_BLOB), :180 (MGP_WIRE_TAIL), :256, :276, :295, :315, :328 and :576 (the five hand-written escapes including Wire_Escape_SetProgramBindings). In a spawn SERVER process any routed call that is not on mgl-srv-apply takes the CLIENT arm and reaches RequireSession (WireTables.cpp:106-119), i.e. Fatal{NoClientSession} - loud, but for the wrong reason and in the wrong process. The audit's c6_ask #4 names only the two SlotAllocator helpers; this site must be in the same ruling.
- MobileGL/MG_Impl/Pipe/PipeFill.cpp:358, :976, :1115, :1206, :1256, :3598 and MobileGL/MG_Remote/Client/GpuWritePending.cpp:227 - six MG_Impl call sites and one MG_Remote one that select their role arm through RunsAsTheServerRole(), i.e. through ServerLoop::OnApplyThread(). Same ask as above, and they are additionally a second MG_Impl -> MG_Remote::Server link that A1-22's build-layout row does not count.
- MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:535 (`PersistentMapTracker::OnServerRole()`), MobileGL/MG_Remote/Client/ClientSession.cpp:1357 and :1376, MobileGL/MG_Pipe/PipeApply.cpp:1355, MobileGL/MG_Backend/DirectGLES/Managers.cpp:230 and :2868, MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:56 and :116 - eight further production sites where ServerLoop::OnApplyThread() IS the role predicate. The MG_State pair is notable on its own: the frontend state layer calls into MG_Remote::Server to ask which role it is in.
- MobileGL/MG_Remote/Client/ClientSession.cpp:758, :802, :814 and MobileGL/MG_Backend/Init.cpp:255 - the TEARDOWN half of A1-13. The client calls ServerSessionInstance().Close() twice and ServerLoopInstance().Stop() once, and MG_Backend::Init's shutdown calls ServerLoopInstance().Stop() unconditionally. Under spawn none of these can reach the other process; ARCHITECTURE.md:504 already rules that the server exits on EOF instead. A1-13 covers only bring-up and its natural_carrier column says nothing about shutdown.
- MobileGL/MG_Impl/Pipe/PipeFill.cpp:348 - `GLContext* LiveContext() { return MG_State::pGLContext.get(); }`. A1-22's link-closure argument is right (I confirmed there are exactly 8 out-of-line `PipeInputs::` member definitions in PipeFill.cpp: IsLive at :2135 and the seven forwarders at :2160, :2178, :2184, :2190, :2202, :2217, :2223), but it names only MG_Impl. Those eight bodies all resolve through LiveContext(), so the transitive dependency is MG_Impl AND MG_State::pGLContext, and MG_State itself calls back into MG_Remote::Server (MipmapStorage.cpp:56, :116). The c6 ruling on 'does the server image link MG_Impl' is really 'does it link MG_Impl + MG_State'.
- MobileGL/MG_Backend/MGPipe/PipeInputs.h:743-745 and :956-958 - two more statements of the 'MG_Impl is not in the server' premise that A1-22 and doc_drift bullet 3 do not name (they cite only :281-284). :744 says 'MGPipeFillAccess lives in MG_Impl, which is the role the server does not have'; :957 says 'NOT enough for a spawned server, where MG_Impl is not in the process'. All three have to move together with whatever c6 rules.
---
## a6.2 进程级静态
**小结**:I re-read the fs-era mechanisms in the tree rather than in the reports, and they hold: `ContextEpoch` (native generation + served serial + execution arm) really does gate both the unpack shadow and the render-state shadow; the raw-depth sampler really is a pure native object rebuilt on native generation; XFB really is role-split and keyed on `BoundStreamOutputLifetimeId`; server liveness really is held by `g_serverContextLive` and driven only from make-current / release / destroy / stop. None of those needs redoing. The delta is elsewhere, and it has one shape: **almost every one of these keys, guards and lifecycles is selected by `MG_Config::Transport != Monolith` (155 sites under `MG_Backend` alone) or by the presence of a `ClientSession` in this process — two facts that stop meaning what they mean today the moment the roles are in two processes.** Concretely: `MGPipeApplierReset`'s layer-2 role guard (PipeApply.cpp:1353-1355) is disarmed in a server-only process because it requires `ClientSession::Active() != nullptr`; `MGPipeServerBlockNoteIdentity` (PipeInputs.cpp:314-315) is a no-op unless `MOBILEGL_IPC_ROLE_SPLIT_STATE=1`, which is OFF by default, so under spawn the server block's `ContextIdentity()` is nullptr with nothing left to fill it; `CountBarrierPull`'s unconditional Fatal arm (PipeInputs.cpp:174) is likewise knob-gated; and `PipeInputs::IsLive()` — the very answer fs re-owned — is **defined in `MG_Impl/Pipe/PipeFill.cpp:2135`**, which answers §3.3's `PENDING a6` question "can the server link without MG_Impl" with a flat no. The new exit boundary adds two owners that do not exist: nothing in a server-only process starts or stops `ServerLoop` (`ClientSession.cpp:704`/`:802` own that today, and the comment at :702 says so), and nothing owns the resolver/callback uninstall order that `ClientSession::Stop` currently sequences. The one thing c6 most needs from this: **write down, as a contract row, which single process-wide fact each of these arms actually keys on under spawn, because `Transport != Monolith` is true in BOTH processes and `ClientSession::Active()` is true in neither of the two the server lives in.**
| A2-1 | blocks-p6 | `ContextEpoch::Server` — the execution-arm third of the epoch key — is `MG_Config::Transport != Monolith`, a process-global config value, not a role fact | MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:18-21 (CurrentContextEpoch); consumers MobileGL/MG_Backend/DirectGLES/Managers.cpp:6497 (ScopedDefaultUnpackState::EnsureShadowSynced) and MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5044-5049 (RenderStateImpl::SyncRenderState) | Nothing, IF the server child keeps `Transport != Monolith`. If ARCHITECTURE.md:488's rule is implemented literally (`mobilegl_server_main` hard-sets `MG_Config::Transport = Monolith` before `Init()`), the epoch's arm bit flips to `false` in the server process and, with it, 155 other `MG_Config::Transport` reads under MG_Backend select the frontend/monolith arm inside the server. | none at the epoch itself; the epoch merely notices the flip and re-publishes once. The damage is in the other 155 sites, | c6 | **overstated** — Say '155 `MG_Config::Transport` reads under MG_Backend, 151 of them the Monolith comparison'. In c6_ask #1, add one sentence: MG_Impl (35) and MG_Remote (23) are deliberately NOT in the re-key set bec |
| A2-2 | already-handled | `ScopedDefaultUnpackState::s_synced` + the six `s_*` pixel-store shadows + `s_epoch`, class-level `static inline` in a header-less .cpp scope | MobileGL/MG_Backend/DirectGLES/Managers.cpp:6472-6549 (class), :6495-6519 (EnsureShadowSynced), :6539 (s_synced), :6541 (s_epoch) | nothing. Under spawn only the server process ever constructs this scope; the native generation and served serial are both server-owned numbers. The only way to break it is A2-1's arm flip. | `s_epoch != epoch` (Managers.cpp:6498) | st | |
| A2-3 | already-handled | render-state shadow family (`g_forceFullRenderStateResync`, `g_hasSyncedRenderState`, `g_syncedRenderStateVersion`, `g_syncedBackendViewport/ScissorBox`, `g_syncedColorMaskAlphaWidenMask`, `g_dualSourceDeclinedBlendStates`) plus the function-local `static ContextEpoch syncedEpoch` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5021-5049 (statics + the epoch gate), :5036-5041 (InvalidateSyncedRenderState), :16209 (the MakeCurrent invalidation that predates fs) | nothing. Both keys are server-owned. `MGB_CTX->GetRenderStateParametersVersion()` at :5054 reads the SERVER block of gPipeInputs, which the applier writes. | `syncedEpoch != epoch` (DirectGLES.cpp:5046) | st | **line-does-not-resolve** — Widen the cite to DirectGLES.cpp:5002-5049. Verdict already-handled is correct. |
| A2-4 | already-handled | raw-depth-fetch sampler: the transport arm's function-local `static UniquePtr<BackendSamplerObject> native` + `static Uint generation`, and the LEGACY arm's two file statics that still hold `SharedPtr<MG_State::GLState::SamplerObject>` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:313-344 (GetRawDepthFetchSampler), :316-321 (the transport statics and the generation rebuild), :82-83 (the legacy MG_State SharedPtr pair); destructor gate at MobileGL/MG_Backend/DirectGLES/Managers.cpp:14690-14708 | nothing on the transport arm. But it is one of the concrete things A2-1 costs: a server child forced to `Monolith` takes the :333-343 branch and constructs an `MG_State::GLState::SamplerObject` — a frontend object — inside the server process, and calls `SyncToBackend` on it, which is the client-registry identity lookup fs deliberately removed. | `generation != g_backendContextGeneration` (DirectGLES.cpp:318) and the destructor's generation compare (Managers.cpp:14 | st | |
| A2-5 | p6-must-handle | `g_Display` / `g_Context` / `g_Surface` / `g_Config` — ONE EGL tuple per process, file-static, and `InitDisplayAndContext` unconditionally destroys the previous one before creating a new one | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:15668-15671 (the four statics), :16012-16013 (`InitDisplayAndContext` opens with `DestroyEGLContext()`), :16079-16095 (InitWindowSurface), :16097-16112 (InitPbufferSurface) | This is the hard answer to "can a server process hold more than one surface or session": **no, and the refusal is not named** — a second `InitWindowSurface`/`InitPbufferSurface` silently destroys the first surface, its context, and (see A2-6) the display. A spawn server that accepted a second session, or a client that created a pbuffer beside its window, loses the first without a log line. | none. There is no `Fatal{...}` and no `if (g_Surface != EGL_NO_SURFACE) refuse` — `DestroyEGLContext()` is simply called | c6 | |
| A2-6 | p6-must-handle | `DestroyEGLContext()` ends with `eglTerminate(g_Display)` — a PROCESS-wide EGL teardown, not a context teardown | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16730-16774, specifically :16761-16773 (`eglMakeCurrent(NO_SURFACE)`, `eglDestroyContext`, `eglDestroySurface`, `eglTerminate`, `g_Display = EGL_NO_DISPLAY`) | A spawn server that serves a second session after a `ReleaseResources` has had its `EGLDisplay` terminated; any EGL object another part of the server process still held (a Magma swapchain, a probe context) is invalidated with it. It also means the server process cannot be 'reset between clients' — `eglTerminate` + `eglInitialize` on the default display is the only path back, and nothing sequences it. | the `MGPipeServerSetContextLive(false)` at :16732-16733 and the generation bumps at :16750/:16754 make every STALE HANDL | sm | |
| A2-7 | blocks-p6 | the served-context serial's two halves: the client's `g_applierResetContextSerial` (a namespace static that is NEVER reset anywhere) and the server's `PipeApplier::m_applierResetSerial`, a member of the LEAKED `ServerSessionInstance()` singleton that `Close()` does not reset either | MobileGL/MG_Remote/Client/WireTables.cpp:642 (declaration, comment says 'the client's own count'), :656 (`record.ContextSerial = g_applierResetContextSerial++`); MobileGL/MG_Remote/Server/PipeApplier.cpp:1149-1157 (the Fatal assertion), MobileGL/MG_Remote/Server/PipeApplier.h:322 (`m_applierResetSerial = 0`), MobileGL/MG_Remote/Server/ServerSession.h:188 (`PipeApplier m_applier`), MobileGL/MG_Remote/Server/ServerSession.cpp:376-382 (the leaked singleton), :656-689 (`Close()`, which resets neither) | A RECONNECTING CLIENT (new client process, same long-lived server process) sends ContextSerial 0 while the server expects N -> `Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}` and `std::abort()` IN THE SERVER. The mirror case (same client process, respawned server after `MOBILEGL_IPC_RESPAWN=1`) sends N while the new server expects 0 -> same abort. So `MOBILEGL_IPC_RESPAWN` as specified (ARCHITECTURE.md:504, 'restart and re-push everything') cannot work without deciding what this serial means across a reconnect. | the Fatal at PipeApplier.cpp:1150 — it fires, loudly and by name. The problem is that it fires on a LEGITIMATE reconnect | c6 | |
| A2-8 | p6-must-handle | `g_sessionOwner` / `g_active` single-session enforcement: a SECOND OBJECT is a named Fatal, but a second `Accept()` on the SAME object — which is what `ServerSessionInstance()` makes the only reachable shape — returns an UNLOGGED error code | MobileGL/MG_Remote/Server/ServerSession.cpp:368 (`g_active`), :372 (`g_sessionOwner`), :421-439 (`Accept`'s CAS; `:425` is the silent `return MOBILEGL_ERR_INVALID_ARGUMENT`, `:426-428` the Fatal), :376-382 (`ServerSessionInstance()`); the one production caller is MobileGL/MG_Backend/Init.cpp:163 | P6 is the first phase in which a second client can physically arrive at a running server. It is refused — but through the `expectedOwner == this` branch, which returns `MOBILEGL_ERR_INVALID_ARGUMENT` with **no MGLOG line at all**. A ServerMain that logs only the rc gets a bare number; the project's 'always a NAMED refusal' rule is not met at exactly the point P6 makes reachable. (The other branch, a genuinely different `ServerSession*`, is correctly `Fatal{RoleViolation, "callback-double-install"}`.) | the CAS itself; but the same-object arm has no name and no log | c6 | **claim-false** — Replace with: 'the only production caller of Accept() is ClientSession.cpp:535; ServerSessionInstance() itself has four production callers (Init.cpp:163, ClientSession.cpp:534, :758, :814)'. Verdict p |
| A2-9 | p6-must-handle | `ServerSession::Close()` re-enterability, and the asymmetry between how it releases the four reverse callbacks (owner-checked) and the process segment resolver (NOT owner-checked) | MobileGL/MG_Remote/Server/ServerSession.cpp:656-689; :663 `Wire::SegmentTable::UninstallProcessResolver()` unconditional, :666-677 the four `== &ServerOnX` identity checks; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:444-449 (Uninstall nulls whoever owns it), :430-442 (Install DOES check, and Fatals) | Nothing about re-entry. What breaks is the ORDER: the resolver's own comment (:661-662 and PipeWireCodec.cpp:445-446) says 'uninstall AFTER the apply thread has joined, never before', and the only thing that sequences that today is `ClientSession::Stop()` — which calls `ServerLoopInstance().Stop()` (join) at ClientSession.cpp:802 and only then `ServerSessionInstance().Close()` at :814. In a server-only process there is no ClientSession to impose that order. See A2-17. | the ordering is a comment, not a check. `Close()` does not assert that the apply thread has joined. | sm | |
| A2-10 | already-handled | `gMGPipeSegmentResolver` + `g_processResolverTable`: one function pointer and one table pointer per process, and `MGPipeHostBytes` returns a SILENT nullptr when the resolver is unset | MobileGL/MG_Pipe/MGPipeHostSpan.h:47 (`inline MGPipeSegmentResolver gMGPipeSegmentResolver = nullptr`), :54-55 (the silent-null arm); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:348 (`g_processResolverTable`), :350-355 (thunk), :430-449 (install/uninstall); installed at MobileGL/MG_Remote/Server/ServerSession.cpp:565, released at :663 | nothing about ownership — the split makes the ruling trivially true, since the client process has no resolver to race. The residual risk is purely the teardown window of A2-9: after `UninstallProcessResolver()` the thunk is null and any still-decoding record resolves to a silent nullptr whose escalation lives in the caller rather than here. | `InstallProcessResolver`'s Fatal (PipeWireCodec.cpp:436-438) on the install side; caller escalation on the resolve side | c6 | |
| A2-11 | later-stage | `gMGPipeCallbacks` has TEN entries; `ServerSession::Accept` installs FOUR and `Close` releases those four. The other six stay null for the life of the server process, and 'null means send nothing' is the documented default | MobileGL/MG_Pipe/MGPipeCallbacks.h:27-52 (the ten), :56-58 (`kMGPipeCallbackCount = 10` + static_assert), :61 (`inline MGPipeCallbacks gMGPipeCallbacks{}` with the 'Null-initialized: a backend that installs nothing sends nothing' comment); installs at MobileGL/MG_Remote/Server/ServerSession.cpp:575-580, releases at :666-677 | nothing new — but note the asymmetry for c6: the install/release identity check is per-entry and owner-checked, so the table survives a session cycle correctly, while the six unfilled rows will silently no-op the first time a spawn lane reaches a P8/P9 path. That is a phase boundary, not a spawn defect. | fv's named refusal on OnGlError; `InstallReverseCallback`'s `Fatal{RoleViolation, "callback-double-install"}` (ServerSes | P9 | |
| A2-12 | blocks-p6 | `MOBILEGL_IPC_ROLE_SPLIT_STATE` is OFF by default and is NOT implied by a real transport — yet spawn IS the dual-block arrangement, structurally and unavoidably | MobileGL/Config.h:540-550 (`Bool RoleSplitState = false`, and the comment calling 0 'the negative control'); MobileGL/ConfigLoader.cpp:400-406 (plain env flag, forced off under PipeVerify); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (`MGPipeRoleSplitActive() = knob && Transport != Monolith`), :297-299 (`MGPipeClientInputs`), :861 in PipeInputs.h (`gPipeInputsClientBlock`) | In the server process there is no client to fill anything, so the arrangement the knob simulates is the ONLY arrangement — but `MGPipeRoleSplitActive()` still answers FALSE unless an operator sets the env var. Three named mechanisms are therefore disarmed in the very configuration they were written for: (a) `CountBarrierPull`'s unconditional Fatal arm (PipeInputs.cpp:174-176); (b) `MGPipeServerBlockNoteIdentity` (see A2-13); (c) `MGPipeClientClearVerbBoundary` selecting the client block. A spawn lane run with the default config is a spawn lane with the role-split detector switched off. | none — the knob is the guard, and it defaults to the disarmed position | c6 | |
| A2-13 | blocks-p6 | `MGPipeServerBlockNoteIdentity()` early-returns unless the role-split knob is armed, so under spawn the server block's `m_contextIdentity` stays nullptr and `m_live` is never written by anyone | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:314-328 (`if (!MGPipeRoleSplitActive()) return;` at :315, the odd-encoded token at :324-327); the rationale is spelled out at MobileGL/MG_Backend/MGPipe/PipeInputs.h:876-885; the consumer is MobileGL/MG_Pipe/PipeInputsSwitch.h:21 (`MGB_CTX_IDENTITY`) read at MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:201-209 | In the server process nothing writes it: the client's SetIdentity is in MG_Impl and never runs there, and this function returns early. `MGB_CTX_IDENTITY` is then nullptr forever, and at DirectGLES.cpp:202 `ctx != g_fbSlotCacheContext` is FALSE on the very first call (both nullptr) so :209 dereferences a null `FbBindingSlot*` — the unnamed crash the rehearsal exists to replace with a named one. Reachable whenever `EsprytSlotTablesEnabled()` is false, i.e. on the supported `MOBILEGL_PIPE_PUSH` A/B arm with bit 5 clear (ARCHITECTURE.md §9.6). Not reachable on the default mask 0x3fff, which sets bit 5 (MG_Pipe/MGPipe.h:164) — and `MOBILEGL_PIPE_LEGACY_MEMOS` defaults ON (CMakeLists.txt:43) so the legacy arm IS compiled. | none in the default spawn config. The knob is what arms it. | c6 | **wrong-verdict** — Downgrade to p6-must-handle. Keep the c6 ask (the identity should not be knob-gated; its token is derived from MGPipeApplierContextSerial(), a server-owned clock) but state honestly that the crash at |
| A2-14 | blocks-p6 | `MGPipeApplierReset`'s layer-2 role guard is armed by `ClientSession::Active() != nullptr` — i.e. by a CLIENT existing IN THIS PROCESS | MobileGL/MG_Pipe/PipeApply.cpp:1353-1360 (`Transport != Monolith && ClientSession::Active() != nullptr && !ServerLoop::OnApplyThread()` -> `Fatal{RoleViolation, "g_applier"}`); the documented exception is at :1348-1351; the state it protects is `g_applier` at :436 | In a spawn SERVER process, `ClientSession::Active()` is permanently nullptr — so the fixture exception becomes the permanent state and the guard NEVER fires. Any direct off-apply-thread `MGPipeApplierReset()` in the server silently resets server state that the wire is supposed to own. This is a gate that can no longer go red for the reason it exists. | the Fatal itself, disarmed by its own first conjunct | c6 | |
| A2-15 | blocks-p6 | `PipeInputs::IsLive()` — the server-liveness answer fs re-owned — and 17 other `PipeInputs::` member definitions are compiled in `MG_Impl/Pipe/PipeFill.cpp`, the CLIENT-role translation unit | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141 (the definition; the transport arm at :2137-2138 forwards to `MGPipeServerContextIsLive()`), :2160-2175 and following (the seven sticky forwarders); the declaration is MobileGL/MG_Backend/MGPipe/PipeInputs.h:262; the claim it contradicts is at PipeInputs.h:281 and :957 ('MG_Impl is not in the server at all') | This is the concrete answer to P6-SPAWN-PLAN §5 / P6-CONTRACT-DRAFT §3.3's `PENDING a6` question: **the server image cannot link without MG_Impl today**, and not because of some far-off glue but because the backend's own `PipeInputs` member functions are defined there. The spawn plan's own §2.3 worry ('blindly forcing Monolith would select frontend glue') is the same fact seen from the other end. | none — it is a link-time fact, and today it always resolves | c6 | **overstated** — Replace '17 other' with 'seven further (the seven F-class sticky forwarders)' and cite PipeFill.cpp:2135, :2160, :2178, :2184, :2190, :2202, :2217, :2223. Keep the verdict blocks-p6 — eight backend-cl |
| A2-16 | p6-must-handle | `PipeInputs::IsLive()` answers `MGPipeServerContextIsLive()` for ANY non-monolith transport, and `g_serverContextLive` is only ever written from server-role code | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2137-2138; MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:305 (`namespace { Bool g_serverContextLive = false; }`), :307-310 (setter), :312 (getter); the six writers are MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16733 and MobileGL/MG_Remote/Server/ServerLoop.cpp:290, :295, :314, :475, :761, :1006 | In the spawn CLIENT process `Transport != Monolith` is also true, so any `MGB_CTX_LIVE` evaluated there would consult a global no one in that process ever sets -> permanently FALSE. I verified that every `MGB_CTX_LIVE` site is under `MG_Backend` (DirectGLES/Managers.cpp and DirectVulkan), which the client process should not be running — so this is latent rather than live, and it becomes live the moment A2-18's `InitSplitRoles` builds a DirectGLES backend in the client process. Separately: `g_serverContextLive` is a plain non-atomic `Bool` written from the apply thread (ServerLoop.cpp:475) and from whatever thread calls `Stop()` (:761). | none; it is a plain Bool with no role or thread assertion | c6 | **overstated** — Say 'seven writers'. No other change; verdict p6-must-handle stands. |
| A2-17 | blocks-p6 | `ServerLoop` — the apply thread, the server's private BackendObject, the current-tuple and the liveness clear — is STARTED AND STOPPED BY CLIENT-ROLE CODE | MobileGL/MG_Remote/Client/ClientSession.cpp:700-704 (Start; the comment at :702 reads 'Under `inproc` the client is what starts the server role, which is why this call is here rather than in some server-side main'), :802 (Stop, step 3 of table 3's order); MobileGL/MG_Backend/Init.cpp:244-255 (`ShutdownSplitRoles`); the singleton is MobileGL/MG_Remote/Server/ServerLoop.cpp:810-812; the teardown it owns is :463-478 (Detach, backend reset on the context-owning thread, `MGPipeServerSetContextLive(false)`, `ForgetCurrentTuple`) | A server-only process has no `ClientSession` and therefore no driver for any of this. Every process-level static that the teardown at ServerLoop.cpp:463-478 is responsible for clearing — the backend object, `g_serverContextLive`, the EGL tuple, and (through `~BackendObject_DirectGLES` -> `DestroyEGLContext`) `g_backendContextGeneration`, the frame-fence ring and `g_backendContextOwnerThread` — has no owner on the EOF exit path ARCHITECTURE.md:504 specifies ('client dies -> server reads EOF -> destroy the native context immediately and exit'). | `ServerLoop::Stop`'s bounded join and its `Fatal{ApplyThreadJoinTimeout}` (ServerLoop.cpp:790-799) are the right mechani | sm | |
| A2-18 | blocks-p6 | `InitSplitRoles()` builds the SERVER's BackendObject (and registers the process-wide resource-ops table) in whatever process has `Transport != Monolith`, then starts a CLIENT session in the same breath | MobileGL/MG_Backend/Init.cpp:151-229; :157-159 (`ServerLoopInstance().CreateBackend`), :163 (`ServerSessionInstance()`), :216 (`ClientSessionInstance().Start`), :228 (`pActiveBackendObject = BackendObject_Remote`); the backend construction is MobileGL/MG_Remote/Server/ServerLoop.cpp:166-191, whose :185-189 comment says `Initialize()` 'registers the resource op table' | Run unchanged in the spawn CLIENT process, this constructs a full `BackendObject_DirectGLES` there and calls `Initialize()`, which loads the driver entry points and installs `g_resourceOps` process-wide — in the process that must not have a DirectGLES backend at all. It is also the function that decides which process holds `ServerSessionInstance()` and `ServerLoopInstance()`. This is the concrete place where 'the child must not become a client' (P6-CONTRACT-DRAFT §3.1) and 'the parent must not become a server' meet, and it is the site a6.3's question lands on too. | `CreateBackend`'s `m_backend != nullptr` guard (ServerLoop.cpp:167-169) only prevents a second one in the same process | sm | |
| A2-19 | already-handled | `PipeInputs::m_serverStampedVerb` — the latch that arms the whole split read path — is cleared by the APPLIER, not only by the client, exactly as the P6 comment demands | MobileGL/MG_Backend/MGPipe/PipeInputs.h:278-288 (the comment that names the P6 hazard by name); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:330-375 (stamp), :377 (`MGPipeServerClearVerbBoundary`); the server-side caller is MobileGL/MG_Remote/Server/PipeApplier.cpp:1328 (stamp) and :1331 (`LeaveApplier` -> clear) | nothing. I checked this specifically because PipeInputs.h:281-287 predicts the failure; the fix is already in the tree. | `PipeApplier::LeaveApplier` (PipeApplier.cpp:1331) | st | |
| A2-20 | already-handled | XFB `g_roleXfbState` — a two-element array indexed by `Transport != Monolith`, per-arm native generation, server keys = `MGPipeApplier().BoundStreamOutputLifetimeId` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1629-1656 (`RoleXfbState`, `ActiveXfbState()`; the role index at :1645, the generation reset at :1647-1650, the role-change `NeedsBind` at :1651-1653), :1659-1677 (`CurrentXfb`, the lifetime key at :1663), :2210-2245 (bind), :2280-2287 (the server's virtual-name-0 arm), :2295-2306 (`OnBackendContextDestroyed`, which clears both arms) | nothing, except through A2-1: the arm index at :1645 is the same `Transport != Monolith` expression, so a forced-monolith server child would silently switch to the GL-name-keyed map and to `scatterProgram`, i.e. to frontend objects that do not exist in that process. | `state.NativeGeneration != g_backendContextGeneration` (:1647); `g_activeXfbRole != role` (:1651) | st | |
| A2-21 | p6-must-handle | `EsprytSlotTablesEnabled()` — a function-local `static const Bool` latched ONCE per process at the first twin lookup, read off `MG_Config::Features.PipePush`, NOT off the transport | MobileGL/MG_Backend/DirectGLES/SlotTables.h:150-153 (the latch), :140 (`ResolveEsprytSlotTablesArm`); the body is MobileGL/MG_Backend/DirectGLES/Managers.cpp:431-471 (the `Fatal{PipeLegacyMemosDisabled}` NoArm stop at :454-458); the default mask is MobileGL/MG_Pipe/MGPipe.h:164 (`0x3fff`, bit 5 set) applied at MobileGL/ConfigLoader.cpp:266 | The two processes parse their own environments. ARCHITECTURE.md:488 specifies scrubbing `MOBILEGL_TRANSPORT` and all `MOBILEGL_IPC_*` from the child's envp — `MOBILEGL_PIPE_PUSH` is NOT in that set, so it inherits through envp but NOT through any value the parent set programmatically after its own parse. If the two arms disagree the client mints `{slot, gen}` handles the server's legacy address-keyed registry cannot resolve, with no gate that compares them. Note also that the `Hello`/`Welcome` fingerprint is build-only (CapsCodec.h) and carries no config. | the build fingerprint does not cover the subsystem mask; `ConfigLoader` logs it but nothing cross-checks | c6 | **line-does-not-resolve** — Cite ARCHITECTURE.md:489. Verdict p6-must-handle is right. Worth adding that MOBILEGL_PIPE_VERIFY is in the same unscrubbed class and interacts with A2-12: ConfigLoader.cpp:401-406 forces RoleSplitSta |
| A2-22 | p6-must-handle | `g_processTeardown` / `EnsureProcessTeardownSentinel()` — the at-exit sentinel every twin destructor consults before touching the driver, registered LAZILY at first registry use | MobileGL/MG_Backend/DirectGLES/Managers.cpp:188-197 (`Bool g_processTeardown`, `std::atexit` registration at :195-196); readers at Managers.cpp:217, :4783, :6068, :9895, :12173, :14691, :15031 and MobileGL/MG_Backend/DirectGLES/SlotTables.h:65; registration sites Managers.h:358, SlotTables.h:275, :355 | The server process's exit path is brand new and `sm` chooses it. If it is `_exit()` (which ARCHITECTURE.md:493's 'EOF detection exits immediately and unconditionally' points at), no atexit handler and no static destructor runs at all — which is safe but means nothing in the tree is responsible for the EGL teardown, so A2-17's owner is mandatory. If it is `exit()`/return-from-main, the lazy registration order relative to the function-local statics registered later (e.g. `GetRawDepthFetchSampler`'s `native`, DirectGLES.cpp:316) decides whether a destructor calls `glDeleteSamplers` after `eglTerminate`. I could not settle the ordering by reading alone. | `InProcessTeardown()` early-outs in seven destructors, plus the independent generation gate at Managers.cpp:14704 which | sm | **line-does-not-resolve** — Change the citation to ARCHITECTURE.md:498. Keep verdict p6-must-handle and confidence uncertain — the open question about atexit ordering is genuinely unsettled by reading, and :498 makes `_exit` the |
| A2-23 | p6-must-handle | `ClientSession::Stop()` reaches into the SERVER role's two process singletons directly, on both its arms | MobileGL/MG_Remote/Client/ClientSession.cpp:758 (`Server::ServerSessionInstance().Close()` on the failed-Start arm), :802 (`Server::ServerLoopInstance().Stop()`), :814 (`Server::ServerSessionInstance().Close()` on the started arm); the ordering argument is at :746-755 and :768-805 | In the spawn client process these calls still compile and still run. `ServerSessionInstance()` would construct a never-accepted `ServerSession` there (Close is then a near no-op), and `ServerLoopInstance().Stop()` would construct a `ServerLoop` in the client process and take its `!m_thread.joinable()` arm (ServerLoop.cpp:755-766), which writes the client process's own `g_serverContextLive = false` and `ForgetCurrentTuple()`. Harmless today, but it is two server-role singletons instantiated in the wrong process, and it is the same function that must instead send a shutdown frame over the socket. | none — the calls are unconditional on the arm | cp | |
| A2-24 | no-action | `g_backendContextGeneration` starts at 1, only ever increments, and `ContextEpoch{}`'s default is `{0, 0, false}` — so the first `CurrentContextEpoch()` in any process can never equal the default-constructed shadow key | MobileGL/MG_Backend/DirectGLES/Managers.cpp:69 (`Uint g_backendContextGeneration = 1`); the single bump is MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16750 inside `DestroyEGLContext`; the default key is MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:11-16 | nothing. Worth recording because it is the reason `st` does not need a 'prime the shadows at server start' step: the epoch's own zero value does it. | the epoch compare itself | st | |
| A2-25 | already-handled | `StagedShadowStore` — 'ONE PER PROCESS', and its copying arm is latched at first use from `MG_Config::Transport != Monolith` | MobileGL/MG_Backend/DirectGLES/Managers.cpp:1154-1165 (the comment and `ServerStaged()`; the constructor argument at :1164 is `MG_Config::Transport != MG_Config::TransportMode::Monolith`); the drop-all at MobileGL/MG_Pipe/PipeApply.cpp:1487-1490 | nothing — it is server-private and its key is server-owned. It is a third transport-latched arm (with A2-21 and A2-1) that a forced-monolith child would flip, and since it latches at FIRST USE rather than at config parse, flipping `MG_Config::Transport` after the first staged resource would strand everything already staged. | the latch itself | st | |
**复审另行指出的遗漏**:
- MobileGL/MG_Remote/Client/ClientSession.cpp:1300, :1397, :1402 — the FOURTH and FIFTH production consumers of MGPipeRoleSplitActive(), which A2-12 does not list. A2-12 names three mechanisms gated on the knob ((a) PipeInputs.cpp:174, (b) :315, (c) :297). The complete production set is five sites: `grep -rn MGPipeRoleSplitActive MobileGL/ | grep -v MG_Test` returns PipeInputs.cpp:174, :298, :315 AND ClientSession.cpp:1300 (`ApplyThreadIsInsideApplier`: `if (MGPipeRoleSplitActive()) return g_roleInsideApplier;`), :1397 and :1402 (`NoteApplyThreadEnteredApplier` / `NoteApplyThreadLeftApplier`: `if (!MGPipeRoleSplitActive()) g_applyThreadInsideApplier.store(...)`). The state behind them is `std::atomic<Bool> g_applyThreadInsideApplier{false}` at ClientSession.cpp:119 — a process-shared atomic WRITTEN BY THE SERVER ROLE AND READ BY THE CLIENT ROLE, which is the purest instance of this item's own subject. With the knob at its default 0, a spawn client process reads an atomic nobody in that process ever writes, so it is permanently false and the R-1 mutual-exclusion Fatal it feeds (RefusePipeInputsTouchWhileApplierOwnsIt, ClientSession.cpp:1304-1394) can never go red there. Honest framing for c6: under spawn that gate SHOULD be vacuous (there is no shared gPipeInputs to race), but it is vacuous by accident of a default-off knob rather than by a role fact — which is A2-12's thesis exactly. This is not a new discovery either: docs/Disaggregated/notes/p5f/f0-statics.md §S13 already classed this scalar 【A】 and predicted it in words ('拆进程后 client 那份恒 false … 否则会漏掉「client 读自己那份永远 false」的假绿'). VERIFIED by reading.
- MobileGL/MG_Remote/Client/CapsMirror.cpp:166-171 (the leaked singleton), CapsMirror.h:95-98 ('Per client context in principle; one per process in P5'), CapsMirror.cpp:91 (`Valid()` is `m_generation != 0`), CapsMirror.cpp:55-87 (Adopt), sole adoption site ClientSession.cpp:448 — a process-scoped singleton holding the SERVER's session-scoped answers (CallMask, FormatCapabilityCache, RendererInfo, apiVersion, BackendType) that ClientSession::Stop() never resets. I checked Stop() line by line: :819-826 resets m_runAheadArmed / m_runAheadLatched / m_presentsSent under the explicit P5e argument 'the LATCH GOES DOWN WITH THE SESSION … a session that starts again must take it again from that session's own first snapshot rather than inherit a verdict about a server that is gone' — and does NOT extend that argument to the mirror the latch was derived from. `grep CapsMirror MobileGL/MG_Remote/Client/ClientSession.cpp` returns only :392, :448, :677, :840; there is no reset anywhere. This is the same shape as A2-7 on the caps side: under MOBILEGL_IPC_RESPAWN=1 (ARCHITECTURE.md:504) the reconnecting client answers R-8's only legal client-side question — CapsMirror::ServerConsumes, whose own comment at CapsMirror.cpp:126-136 says a placeholder mirror consuming nothing is 'the safe direction' and emitting to a server with no consumer is 'the one that loses uploads' — from the DEAD server's CallMask, and Valid() stays true so WarnPlaceholder never fires. The asymmetry worth naming to c6: f0-statics §S13 classed BOTH `g_applierResetContextSerial` (WireTables.cpp:642) and `CapsMirror.cpp:169` as 【B】 'client-side, per-process is fine'; a6.2 correctly reopened the first (A2-7) and did not reopen the second, although the respawn boundary is new to P6 for both. VERIFIED by reading; the consequence is inferred and depends on what MOBILEGL_IPC_RESPAWN's 're-push everything' is specified to do.
- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:312-322 (`MGPipeSlotAllocator::Reset()`) and :324-341 (`MGPipeSlots()`) — the client's {slot, gen} handle allocator is a process-lifetime singleton whose own comment at :331-338 says 'NEVER DESTROYED, deliberately (one allocation for the life of the process)', and `Reset()` has ZERO callers in the whole tree (`grep -rn 'Slots()\.Reset\|SlotAllocator.*Reset'` finds only the definition). Same family as A2-7: a process-scoped counter/space that no session boundary resets, and here the reset that exists is dead code. Under MOBILEGL_IPC_RESPAWN=1 the client keeps its entire slot and generation space across the reconnect while the fresh server's twin registries start empty, so whether the respawn works at all depends on 're-push everything' re-emitting every resource_create with the SAME client-minted handles. c6 should say which, and either wire Reset() into the respawn path or delete it so it is not mistaken for a mechanism. VERIFIED that the allocator is process-scoped and Reset() is uncalled; the respawn consequence is inferred.
- MobileGL/MG_Backend/MGPipe/PipeInputs.h:496, :515, :526 — the three `m_serverStampedVerb`-gated dual-arm getters (GetSamplingResolutionGeneration at :496, GetTextureBindGeneration at :515, GetTextureContextId at :526) each read `if (m_serverStampedVerb) return MGPipeApplier*Serial();` and otherwise fall back to a CLIENT-FILLED member (m_textureBindGeneration :517, m_textureContextId :528). This is A2-13's shape one level down and A2-13 does not cover it: in a server-only process the fallback member is never written by anyone, and PipeApplier::LeaveApplier (PipeApplier.cpp:1331) clears the stamp on every applier exit, so ANY backend read outside a stamped verb — a control-request path, InitCapabilities, a surface op, the sticky-forward exemption family PipeInputs.h:955-958 names — takes the non-stamped arm and keys off a permanently-zero value. These three answers key the whole DirectGLES context-scoped memo family: g_observedUnitBindingsContextId (DirectGLES.cpp:3179), g_unitTextureSyncListByHandleContextSerial (:3415), g_unitTextureSyncListContextId (:3423), g_fboTextureSyncListContextId (:3461), g_fboAttachmentSyncListContextSerial (:3487). Those memos are each double-keyed on native generation as well (:3419, :3425, :3462, :3488), so a stale read is bounded rather than catastrophic — which is why I rate it p6-must-handle for the contract rather than blocks-p6. VERIFIED the code shape by reading; reachability outside a stamped verb is inferred, and settling it needs the ServerMain control path sm has not written.
- MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp:55-70 and MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp:34-35 — the query and sync registries (g_queryObjectsMutex / g_liveQueryObjects / g_nextQueryId / the four active-query ids / g_activePipelineStatisticsQueryIds; g_syncObjectsMutex / g_liveSyncObjects) are per-context BY SPEC and stored per-process. This is the literal statement of the item's title and a6.2 has no row for it at all, although docs/Disaggregated/notes/p5f/f0-statics.md §S12 already recorded it as a multi-context debt. Under spawn they stay in the CLIENT process, which is the right process, and P6-CONTRACT-DRAFT §9.2 declines multi-context (it moves to P12), so the correct verdict is later-stage/P12 — but the item should CLASSIFY them rather than omit them, so a6's successor does not re-discover them. Note the audit also used file paths that no longer exist: f0-statics cites `GL_Query.cpp:55-66` and `GL_Sync.cpp:34-37` without the Query/ and Sync/ subdirectories they now live in. VERIFIED by reading.
- MobileGL/MG_Backend/Init.cpp:82 — a tree comment the audit had every reason to catch while working in this exact function and did not: it asserts 'MGPipeSetResourceOps has exactly one caller in the whole tree and it is Managers.cpp:2594'. At b3b9c2ee there are TWO callers and neither is at that line: DirectGLES/Managers.cpp:3137 and DirectVulkan/Renderer/VkBufferManager.cpp:170 (with the matching owner-checked unsets at Managers.cpp:3149-3150 and VkBufferManager.cpp:429-430). Low severity — it does not change any verdict — but it sits in the middle of InitSplitRoles' rationale block, which is the text c6 will read when splitting the function per A2-18. Two smaller instances of the same rot, both in comments the audit quotes from: PipeApplier.cpp:1160 cites 'the g_applier this role owns (PipeApply.cpp:409)' and PipeFill.cpp:822 cites 'a PROCESS-WIDE global (PipeApply.cpp:402)', while `MGPipeApplierState& g_applier` is at PipeApply.cpp:436 and `const MGPipeResourceOps* g_resourceOps` at :442. VERIFIED by reading.
- NEGATIVE RESULT worth recording so c6 does not chase it, and so the integrator knows this was checked rather than skipped: g_resourceOps (PipeApply.cpp:442) is NOT a gap. It is the most obvious candidate in this family — a process-wide table the server registers that client code reads — but c1's R-8 work already moved every client-side consumer to the caps mirror, and the code names the spawn case by name: PipeFill.cpp:819-828 ('under spawn the client process has no backend at all, so the read answers null and five record families stop emitting - silently'), with the moved arms at :834-835 (MGPipeResourceSubsystemEnabled), :843-848 (MGPipeResourceOpsHaveSubDataResident) and :1427-1431 (P4aFamilyHasItsConsumer), and the server-side honesty check at Init.cpp:119-131 (Fatal{ConsumerMaskLie}). Likewise g_clientTablesUninstalled (WireTables.cpp:104) IS session-cycle-safe: it is reset to false at WireTables.cpp:730 and :800, unlike the two serials in A2-7. Both were checked and found already-handled.
---
## a6.3 gPipeInputs 角色归属与反递归
**小结**:Today a record consumer selects its server arm with ONE predicate spelled 238 non-test times: `MG_Config::Transport != MG_Config::TransportMode::Monolith`. It is process-global, latched at `ConfigLoader::Init`, never thread-dependent. Beside it live exactly two other real predicates - `ServerLoop::OnApplyThread()` (ServerLoop.h:265, one relaxed load of `g_applyThreadKey` plus a thread-pointer compare) and `ClientSession::Active() != nullptr` - plus nine aliases/wrappers of those three. The decisive finding is that ARCHITECTURE.md:488's instruction ("`mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`") is not merely suboptimal, it is unsound at named sites I opened: it flips `ContextEpoch::Server` to false and `Served` to 0 (ContextEpoch.h:19), constructs BOTH server staged stores with `copies=false` (Managers.cpp:1164, StagedTextureStore.h:475 - latched function-statics, unrecoverable after first use), re-enables the frontend VAO walk in the server (DirectGLES.cpp:2483/2576), disarms three role Fatals (SlotAllocator.cpp:30/50, MipmapStorage.cpp:55, BufferObject.cpp:63), and - worst, because it is SILENT - makes `PipeInputs::IsLive()` (PipeFill.cpp:2137) answer from the child's null `pGLContext` instead of `MGPipeServerContextIsLive()`, turning all 58 `MGB_CTX_LIVE` guards into a quiet "no context" in a process that has one. `Dial = No` is the right structural catch, but it is NOT sufficient at one site: `Init.cpp:266` is a two-way `Transport != Monolith` whose else-arm is the monolith backend switch at `Init.cpp:305`, so a child with `Dial=No` and `Transport=Spawn` still needs a THIRD `Role == Server` arm there or it falls into exactly the trap. gPipeInputs itself is already clean: the P5f dual-block census (`Harness/dualblock-expected-fatals.txt`) is EMPTY, which is in-process proof that the server block is self-sufficient - so the one thing c6 most needs from this audit is the decision at Init.cpp:266, expressed as a three-valued Role and not as a boolean Dial.
| A3-1 | blocks-p6 | `MG_Config::Transport != TransportMode::Monolith` - the dominant server-arm selector. 238 non-test occurrences (311 with tests); 67 in DirectGLES.cpp, 27 VulkanRenderer.cpp, 24 Managers.cpp, 19 PipeFill.cpp, 16 DirectVulkan.cpp, 11 PipeApply.cpp, 9 ClientSession.cpp, 8 in MG_State. | MobileGL/Config.h:455 (declaration); MobileGL/ConfigLoader.cpp:33 (definition, plain non-atomic, default Monolith); representative consumers MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2483, MobileGL/MG_Backend/DirectGLES/Managers.cpp:2867 | In the child the two facts part company. If the child forces Monolith (ARCHITECTURE.md:488), all 238 sites select FRONTEND GLUE in the one process that has no frontend. If the child keeps non-Monolith, the same 238 sites are right but the predicate now also says "dial out" at Init.cpp:266, which is the fork chain. | none - it is a plain enum load, and the pull build folds it to constexpr Monolith (Config.h:566), so no gate sees it | c6 | **overstated** — Replace 238/311 with 178 non-test predicate sites (179 incl. the integration harness; 177 excluding the Config.h:564 comment), and 208 total comparisons against Monolith counting the 30 `== Monolith` |
| A3-2 | p6-must-handle | `ServerLoop::OnApplyThread()` - the thread-identity predicate. Body is `g_applyThreadKey.load(relaxed) != 0 && key == CurrentThreadKey()`; the key is the thread pointer (TPIDR_EL0) on aarch64. | MobileGL/MG_Remote/Server/ServerLoop.h:265-271; key published at MobileGL/MG_Remote/Server/ServerLoop.cpp:344 (ApplyThreadMain) and cleared at :528 | In the server process the ROLE is the whole process. If ServerMain drives the applier on its main thread rather than through ServerLoop::Start, g_applyThreadKey stays 0 and every OnApplyThread() guard answers FALSE in the server - the guards silently stop guarding. If ServerMain does start a ServerLoop, the guards are right on that thread but still answer FALSE on the server's control-pump / socket-reader threads, which are also the server role. | none. The header argues the key is never 0 for a LIVE thread, not that the role implies a thread. | sm | |
| A3-3 | p6-must-handle | `MGPipeRoleSplitActive()` = `MG_Config::Ipc.RoleSplitState && Transport != Monolith`. Deliberately NOT latched (two global loads per verb). | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 | Nothing breaks - it becomes MOOT, which is its own problem. Under spawn the two blocks are in two address spaces whatever the knob says, so the knob can no longer change behaviour in either process, and scripts/ci/dualblock_negative_control.sh's red form (ROLE_SPLIT_STATE=0 must turn the lane red) cannot go red in a spawn lane. This is a FOURTH question - "do the two roles share one storage object" - that Role/Dial/SplitActive cannot express. | scripts/ci/dualblock_negative_control.sh - and that is exactly the gate that stops being able to fail | t6 | |
| A3-4 | already-handled | gPipeInputs storage and the dual-block selection. `gPipeInputs` is the SERVER-role block; `gPipeInputsClientBlock` exists only in a disaggregated build; `MGPipeClientInputs()` returns the client block iff MGPipeRoleSplitActive(). Both are leak-at-exit `*new PipeInputs()` because their O-class members are SharedPtrs to MG_State frontend objects. | MobileGL/MG_Backend/MGPipe/PipeInputs.h:837 (gPipeInputs), :861 (gPipeInputsClientBlock), :895 (the non-disagg fold); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:297-299 (selection) | Nothing, and that is the useful answer. In the SERVER process gPipeInputs is the only block and is wholly server-written; in the CLIENT process gPipeInputs (or the client block) is fill-written and read by NOBODY, because no backend is linked into the reading path there. The `gPipeInputsClientBlock` allocation and the selection branch become dead weight in both processes. | MG_IntegrationTest/Scenarios/DualBlockScenario.cpp asserts blocksDistinct and that a client verb boundary does not move | c6 | |
| A3-5 | already-handled | Who WRITES gPipeInputs in the server role: the applier through MGPipeApplyAccess, unconditionally on `gPipeInputs` (never through MGPipeClientInputs). | MobileGL/MG_Pipe/PipeApply.cpp:1619 (BindRenderState), :1647 (SetDynamicState), :1656 (SetPixelPackState), :1668 (SetContextValues), :1682 (SetPatchState), :1741, :1822 | nothing - these are already address-space-local server writes. They are the reason gPipeInputs is self-sufficient in a second process. | n/a | c6 | |
| A3-6 | already-handled | Who STAMPS gPipeInputs: MGPipeStampAccess, from PipeApplier. The stamp fills FilledGen[i] with the new serial for RECORD_SUPPLIED/APPLIER_DERIVED fields inside the verb's class mask and ZERO for everything else; FilledGen==0 is "never filled" and is Fatal on read. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:330-375 (stamp), :377 (clear), :325 (SetIdentity); called from MobileGL/MG_Remote/Server/PipeApplier.cpp:1328 (StampVerbBoundary) and :1331 (LeaveApplier); freshness rule MobileGL/MG_Pipe/generated/PipeFilled.inc:421-426 | nothing structurally, but it is the ONLY writer of the server block's stamps in the child. Every read before the first stamp has FilledGen==0 and therefore Fatals - which is correct and loud. PipeInputs.h:277-285 already states that latching ServerStampedVerb TRUE is wrong "for P6, where MG_Impl is not in the server at all", and the clear at PipeApplier.cpp:1331 is what makes that true. | MGPipeInputUnfreshRead (PipeInputs.cpp:381-413) - poison Fatal or Fatal{RoleViolation} | st | |
| A3-7 | already-handled | Who READS gPipeInputs: 383 MGB_CTX* occurrences in MG_Backend - 310 `MGB_CTX->`, 58 `MGB_CTX_LIVE`, 3 `MGB_CTX_IDENTITY`. All resolve to `&gPipeInputs`. | MobileGL/MG_Pipe/PipeInputsSwitch.h:19-21; densest consumers MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp (112), MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp (135) | nothing on the read side. The P5f dual-block census is EMPTY (MobileGL/MG_IntegrationTest/Harness/dualblock-expected-fatals.txt, "the expected set is empty after fc -> fe -> fm integration"), i.e. the whole integration-split + integration-magma-split lane already runs green with the two blocks separated and MOBILEGL_IPC_STRICT_ERRORS=1. That is in-process PROOF that no backend read reaches the client fill. | dualblock-expected-fatals.txt is two-sided (split_log_paths.py expect-fatal fails on a new pair AND on a listed pair tha | st | **overstated** — Keep the verdict (already-handled) and the 58. Recompute the other four numbers, or drop them - they are decoration on a conclusion the empty census already carries. Same recomputation invalidates doc |
| A3-8 | blocks-p6 | `PipeInputs::IsLive()` - the liveness forwarder, whose BODY branches on Transport and whose definition lives in MG_Impl. Non-monolith -> `MGPipeServerContextIsLive()` (server-owned, P5f fs). Monolith -> `LiveContext() != nullptr`, i.e. `MG_State::pGLContext`. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141; consumed as MGB_CTX_LIVE at MobileGL/MG_Pipe/PipeInputsSwitch.h:20, 58 read sites in MG_Backend | THE SHARPEST force-Monolith failure. With the child forced Monolith, IsLive() reads `MG_State::pGLContext`, which is null in the server process, so all 58 MGB_CTX_LIVE guards answer FALSE and every context-guarded backend path silently early-returns. IsLive() carries NO MGP_INPUT_CHECK (PipeInputs.h:259-262, it is "not a field"), so this is a silent wrong picture, never a named Fatal. | none - IsLive is outside the poison, by design | sm | |
| A3-9 | p6-must-handle | Eight out-of-line `PipeInputs::` member definitions live in MG_Impl, not MG_Backend: IsLive, GetBufferBindingPointCount, GetProgramObject, GetTextureObject, HasOpenTransformFeedbackSpan, InvalidateCompileEnv, ValidateProgramName, RecordError. Six of them re-ask `Transport != Monolith` in their own bodies. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135, :2160, :2178, :2184, :2190, :2202, :2217, :2223 | A server image that does not link MG_Impl has eight undefined symbols reachable from the backend's own MGB_CTX reads. Under force-Monolith the ones that DO link take the client path and answer from a null GLContext: GetBufferBindingPointCount returns 0 instead of kMGPipeMaxBufferBindingPoints, GetProgramObject/GetTextureObject return an empty SharedPtr, RecordError MGLOG_E_ONCEs and DROPS the error instead of firing the reverse-channel Fatal at PipeFill.cpp:2231-2235. | none. MGPipeStickyForwardPull (PipeInputs.cpp:442-452) only counts these inside a server-stamped verb, and with 0 BARRIE | a6 | **overstated** — Say FIVE re-ask the predicate, and split the other three out as 'null-GLContext in any child, independent of the force-Monolith decision' - they argue for the link/ownership question (a6.5), not for t |
| A3-10 | blocks-p6 | `Init.cpp`'s single hook: `if (Transport != Monolith) { InitSplitRoles(); ... return; }` and the ELSE-arm, which is the monolith backend switch. | MobileGL/MG_Backend/Init.cpp:266 (the hook), :151-230 (InitSplitRoles), :216 (ClientSession::Start - the dial), :305-317 (the monolith switch) | THE ANTI-RECURSION SITE, and a boolean `Dial` is not enough here. `Dial = No` + `Transport = Spawn` makes the condition at :266 still TRUE, so the child dials. Making the condition `ShouldDial()` instead makes it FALSE in the child, which drops it into the monolith switch at :305 and constructs `BackendObject_DirectGLES` on the app thread with no ServerLoop, no applier and no stamp - the exact frontend-glue trap. This site needs a THREE-VALUED Role, not a boolean. | none. InitSplitRoles' failure path sets pActiveBackendObject=nullptr (Init.cpp:270-273) and is loud, but the Role==Serve | c6 | |
| A3-11 | blocks-p6 | `ContextEpoch::Server` - `Transport != Monolith` used as a CACHE KEY VALUE, not a branch. The epoch is {native generation, served-context serial, Server}; with Server false the Served half is forced to 0. | MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:17-21 | Force-Monolith gives the SERVER `Server=false, Served=0`, so the served-context serial stops invalidating anything and a replaced context reuses the previous one's sync state - which is exactly what negative control S4 exists to falsify. Separately, if c6 rewrites this as `MGPipeServerArm()` carrying the `OnApplyThread()` disjunct, the value becomes THREAD-dependent and the same static memo is keyed two ways under inproc. | P6-SPAWN-PLAN §7 S4 ("临时关闭 spawn 接线中继承的 P5f epoch 失效") - but S4 tests the epoch, not the Server bit that keys it | c6 | |
| A3-12 | blocks-p6 | `ServerStaged()` - the server's buffer staged-shadow store, a function-static constructed ONCE with `Transport != Monolith` as its `CopiesIntoServerStorage` constructor argument. | MobileGL/MG_Backend/DirectGLES/Managers.cpp:1161-1166 (argument at :1164); class at MobileGL/MG_Remote/Server/StagedShadow.h:34-35 | Force-Monolith constructs it with `false` in the server process, so the server keeps NO authoritative bytes and falls back to a frontend MappedData that does not exist there. It is LATCHED, so nothing later can correct it. | none at the construction site. StagedShadowStore takes the flag as a parameter precisely so a unit case can compare the | c6 | **line-does-not-resolve** — Change the why-it-works cite from Managers.cpp:2247-2252 to Managers.cpp:1155-1157. The verdict (blocks-p6, latched, unrecoverable) is correct and survives. |
| A3-13 | blocks-p6 | `ServerStagedTexture()` - the same shape for textures: a function-static constructed once with `Transport != Monolith`, consumed by BOTH backends (Espryt's Managers.cpp and Magma's VulkanRenderer.cpp). | MobileGL/MG_Remote/Server/StagedTextureStore.h:473-477 (argument at :475) | same as A3-12, and it is worse because two backends share the one store - a wrong latch in the child silently disables the texture staging both of them read. | none in production wiring (StagedTextureStore.h:92-95 says so out loud) | c6 | **claim-false** — The guard is not 'none' - it is MG_Test/Wire/StagedTextureStoreTest.cpp:740-743 + :445-453, an in-process red-once that WOULD catch a force-Monolith latch. The spawn-specific problem is therefore shar |
| A3-14 | p6-must-handle | `ActiveXfbState()` - `const Int role = Transport != Monolith` used as an ARRAY INDEX into a 2-element role-partitioned XFB state table, with `g_activeXfbRole` tracking transitions and forcing a rebind on change. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1641-1655; consumers at :1662 (CurrentXfb), :1807 (BindScatterCaptureBuffer), :1848 (ScatterCapturedRecords), :2212 (BindTransformFeedback), :2257 (DeleteTransformFeedback) | Force-Monolith puts the server on index 0, whose keys are GL NAMES it never receives (the server's identity is `MGPipeApplier().BoundStreamOutputLifetimeId`, DirectGLES.cpp:1664). Also a decomposition misfit: rewriting `role` as `MGPipeServerArm()` with the OnApplyThread disjunct would make the index thread-dependent under inproc and split one process's XFB objects across both slots. | none - `g_activeXfbRole != role` only forces a rebind, it does not refuse | c6 | |
| A3-15 | p6-must-handle | The "MONOLITH ONLY" frontend-glue gates: `if (Transport != Monolith) return true;` guarding client-side-vertex-array staging, which reads `MGB_CTX->GetBoundVertexArray()` and the frontend binding slots. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2483 (SyncClientSideVertexArraysForFetch), :2544 (ForIndirectFetch), :2576 (ForIndexedFetch); same shape at :4297, :4416, :8964 | Force-Monolith re-enables the whole family in the server. Here the failure IS named: GetBoundVertexArray is a FATAL row (MG_Pipe/FieldOwnership.def:27), so the read aborts with Fatal{UnmigratedPipeInput,"GetBoundVertexArray@<verb>"} - but only once the applier has stamped a verb. Between stamps FilledGen==0 also Fatals, so this class is loud rather than silent. It is still 40-odd bodies of dead-and-dangerous code re-armed in the wrong process. | MGP_INPUT_CHECK -> MGPipeInputUnfreshRead (PipeInputs.cpp:381) -> Fatal | c6 | **wrong-verdict** — Move DirectGLES.cpp:8962-8970 out of A3-15 and into A3-16's disarmed-Fatal family (it is the fifth member there, alongside the one named in `missed`). Fix FieldOwnership.def:27 -> :28. The remaining f |
| A3-16 | p6-must-handle | Role-violation Fatals of the shape `if (Transport == Monolith) return; if (!OnApplyThread()) return; Fatal;` - the tree's canonical "SplitActive AND ServerArm" conjunction. | MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-43 (MGPipeRefuseAllocatorFromApplyThread) and :46-56 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread); MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:54-67 (RefuseLegacyTextureArmFromApplyThread); MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:62-66 (RefuseLegacyBufferArmFromApplyThread, via PushIsArmed/OnServerRole) | Force-Monolith makes the FIRST conjunct false, disarming every one of these in the process where the client allocator genuinely does not exist. With Transport kept non-Monolith, the SECOND conjunct is only true on a ServerLoop apply thread - the server's control-pump and socket-reader threads slip through. The review's `MGPipeServerArm() = Role==Server // (Role==Client && OnApplyThread())` FIXES both halves: in the child Role==Server makes the guard fire on every thread. This is the decomposition's best fit. | these ARE the guards | sm | |
| A3-17 | p6-must-handle | `MGPipeApplierReset`'s three-conjunct guard: `Transport != Monolith && ClientSession::Active() != nullptr && !OnApplyThread()`. The middle conjunct is a FOURTH axis - "is there a live wire" as distinct from "is a transport configured". | MobileGL/MG_Pipe/PipeApply.cpp:1354-1362; the axis is documented at PipeApply.cpp:41-45 and ClientSession.cpp:455-458 | `ClientSession::Active()` is null in the SERVER process for the whole of its life - there is no client session there - so this guard is unconditionally disarmed in the child. It is a concrete counterexample to the three-predicate decomposition: SplitActive/ServerArm/ShouldDial cannot express "a peer session is live", and the tree already needs that axis in two places. | this is the guard; it stops being able to go red in the server process | c6 | |
| A3-18 | p6-must-handle | `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` - the gPipeInputs role guard. Opens with `if (Transport == Monolith) return;` then consults `ApplyThreadIsInsideApplier()`, a process-global atomic raised by the applier. | MobileGL/MG_Remote/Client/ClientSession.cpp:1303-1392 (entry at :1303, SplitActive test at :1320, the two Fatals at :1358 and :1387); flag at :119 and :1396-1404 | In the CLIENT process `g_applyThreadInsideApplier` is never set by anyone, so `ApplyThreadIsInsideApplier()` is permanently false and both Fatals become unreachable - a gate that can no longer go red for the reason it exists. It must be retired BY ROLE ("this process has no applier") and named, not left silently satisfied by an always-false global. | itself - and that is the problem | c6 | **overstated** — Keep verdict p6-must-handle. Restate the mechanism as: the flag is thread_local under role-split (:123) and a never-written process atomic otherwise (:119) - both answer false in a spawn client. State |
| A3-19 | p6-must-handle | `OnFrontendStateObjectDestroyed`'s emit fork: `Transport != Monolith && !OnApplyThread()` -> emit object_death as a RECORD; else fall through to the direct twin-release switch. | MobileGL/MG_Backend/DirectGLES/Managers.cpp:229-231 | In the SERVER process any non-apply thread satisfies both conjuncts and would try to EmitObjectDeathRecord from the server - publishing into the ring the server is consuming. Rewriting `!OnApplyThread()` as `!MGPipeServerArm()` with the Role disjunct fixes it; rewriting it as `!OnApplyThread()` alone does not. | none at this site | sm | |
| A3-20 | p6-must-handle | `MGL_BACKEND_SLOT_CAP` / `_LOCAL` / `_PTR_CAP` / `_PTR_LOCAL` - `Transport != Monolith` selecting "ask the CapsMirror (the REMOTE server's published caps)" vs "read the local backend function table". | MobileGL/MG_Remote/Client/SlotCaps.h:110-136; users MobileGL/MG_Impl/GLImpl/{Drawing,Getter,Query,Sync,Texture}, MobileGL/MG_Backend/Init.cpp | This predicate is asking `Role == Client`, not SplitActive. In the child (Role==Server, Transport non-Monolith) it answers "ask the mirror", and the child's CapsMirror is a PLACEHOLDER - CapsMirror.cpp:44-52 warns once and answers placeholder at generation 0. Reachability in the child depends on whether any MG_Impl GL entry point runs there; I did not establish that it does. | CapsMirror::WarnPlaceholder (CapsMirror.cpp:44-52) - a warning, not a refusal | c6 | |
| A3-21 | p6-must-handle | The raw `g_applyThreadKey != 0` read - a THIRD question neither SplitActive nor ServerArm can ask: "has an apply thread ever run in this process". | MobileGL/MG_Backend/DirectGLES/Managers.cpp:2865-2884 (the buffer-mutation-epoch pin); the reasoning is written out at :2843-2855 | In the server process EVERY bump is legitimate before the applier starts and none is after, but `g_applyThreadKey != 0` still only answers "is a ServerLoop apply thread up", not "has this process finished bring-up". If ServerMain does not use ServerLoop, the pin never arms. The decomposition needs a bring-up/steady-state axis or this pin silently stops covering the server. | g_bufferMutationEpochBumpsOffTheApplyThread counter + Fatal under MOBILEGL_IPC_STRICT_ERRORS | st | |
| A3-22 | p6-must-handle | The aliases: three wrapper functions whose whole body is one of the two real predicates, so a c6 rewrite must find them by BODY, not by name. | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526-528 (PushIsArmed == Transport != Monolith) and :535 (OnServerRole == OnApplyThread); MobileGL/MG_Remote/Client/WireTables.cpp:87 (RunsAsTheServerRole == OnApplyThread); MobileGL/MG_Remote/Client/GpuWritePending.cpp:125-127 (GpuWriteSetIsClientSide == Transport != Monolith) | Each inherits its underlying predicate's failure exactly. `PushIsArmed()` in particular gates BufferObject's legacy-arm refusal (BufferObject.cpp:63), so force-Monolith disarms that Fatal through an alias rather than through the obvious spelling. | none | c6 | |
| A3-23 | p6-must-handle | The subsystem-bit wrappers: `Transport != Monolith && <PipePush bit>` - six helpers that conjoin the server-arm question with a feature bit so an operator can put a family back on the frontend walk. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:272-274 (FramebufferRecordArmIsMandatory), :3385-3388 (UnitTexturesByHandle), :660-661 (ResolveBufferBindingSubsystemArm); MobileGL/MG_Backend/DirectGLES/Managers.h:740-742 (ProgramHandleArm), :1146-1150 (VertexInputReadsRecords) | Force-Monolith makes all six answer "frontend walk" in the server. Under the decomposition they become `MGPipeServerArm() && bit`, which is right - but note that these are the sites where the server arm is legitimately OPTIONAL, so `ServerArm()` must not be conflated with "the record path is mandatory". | ResolveBufferBindingSubsystemArm refuses bit 13 without bit 7 by name (DirectGLES.cpp:663-672) - a good model for the sh | c6 | |
| A3-24 | blocks-p6 | `MG_Config::TransportMode` itself: 5 enumerators in one Uint8, plus `TransportEndpoint` beside it. It conflates four independent facts and stores two different lifetimes. | MobileGL/Config.h:443-449 (enum), :455-457 (Transport + TransportEndpoint); MobileGL/ConfigLoader.cpp:33-34 (definition, default Monolith "so an early read sees Monolith rather than a torn enum") | The four conflated facts are: (1) does a wire exist; (2) WHICH ROLE this process plays - not encoded at all, inferred from (1); (3) whether and HOW this process dials (Spawn=fork+exec, UnixSocket=connect, NamedPipe=connect, InProcess=construct the peer here) - and P6-ENDSTATE-REVIEW:144-146 says that under AVF the SERVER dials and the CLIENT listens, which no enumerator can express; (4) the MEDIUM, carried half in the enum and half in TransportEndpoint. Adding Spawn costs exactly ONE predicate (ClientSession.cpp:482); adding the ROLE axis costs the other 238 nothing ONLY IF the child keeps Transport != Monolith. | ConfigLoader.cpp:347-350 names an unknown value; there is no guard on the conflation | c6 | |
| A3-25 | p6-must-handle | `MOBILEGL_TRANSPORT=spawn` parses and is then RESET to Monolith - so `MG_Config::Transport` can never hold TransportMode::Spawn at this head, and the sequencing of the integration commit matters. | MobileGL/ConfigLoader.cpp:339-346 (the named refusal inside InitTransport, which spans :319-351) | If ConfigLoader stops refusing spawn BEFORE `so`/`sm` land, all 238 sites flip to the server arm in a process that has no working session: Init.cpp:266 enters InitSplitRoles, which creates the server's private backend at :158, registers the resource op table, and only then hits the refusal at ClientSession.cpp:482 and returns false - leaving pActiveBackendObject null with a half-built server behind it. P6-SPAWN-PLAN §5 already sequences the ConfigLoader change last; this row is the concrete blast radius for why. | ClientSession.cpp:482-489 named refusal; Init.cpp:270-277 refuses to fall back | c6 | |
| A3-26 | p6-must-handle | Config is a SNAPSHOT of `environ` taken at ConfigLoader::Init, filtered to LIBGL_/MOBILEGL_ prefixes, and the map is destroyed immediately after. So the envp scrub is total for the child - and totally ineffective for anything the child later execs. | MobileGL/ConfigLoader.cpp:42-59 (IsAcceptedPrefix + the environ walk), :430-445 (Init order: InitBackendType, InitFeatures, InitTransport, InitIpc, then map.reset()); called once from MobileGL/Init.cpp:118 | This is what sizes the Dial=No-vs-envp-scrub question. `Dial` is in-process state and is NOT inherited; `MOBILEGL_TRANSPORT` IS. So the scrub catches exactly the cases Dial=No cannot: a GRANDCHILD that loads libMobileGL fresh (ARCHITECTURE.md:498 already names one - HeadlessGL's fork pre-check forks a child that runs full EGL bring-up), a respawn by MOBILEGL_IPC_RESPAWN (registered for P6+ at ARCHITECTURE.md:639), and a platform restart of the child. Dial=No catches what the scrub cannot: a second exec path added later that forgets to scrub, and any configuration source that is not envp. Also: Dial must be established BEFORE Init.cpp:118, because after ConfigLoader::Init the map is gone and a later setenv is invisible. | P6-SPAWN-PLAN §7 S3 ("子进程 envp 未剔除" -> 第二道保险挡住递归 spawn 并具名) | sm | |
| A3-27 | p6-must-handle | The DualBlock scenario asserts an IN-PROCESS fact about the server block - `serverIdentity != nullptr` - that is only ever set by the server's own stamp/attach. | MobileGL/MG_IntegrationTest/Scenarios/DualBlockScenario.cpp:69-73 (the assertion); MobileGL/MG_IntegrationTest/Harness/DualBlockPeek.cpp:34-41 (the peek); identity written at MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:314-328 | Run from the CLIENT process under spawn, `gPipeInputs.ContextIdentity()` is nullptr - nothing in that process ever calls MGPipeServerBlockNoteIdentity - so the case fails for a reason that is not a defect. P6-SPAWN-PLAN §6 G2/G14 requires the integration-spawn case-name set to be name-for-name identical to integration-split, so this case cannot simply be dropped. | the assertion itself - it goes red for the wrong reason | t6 | |
**复审另行指出的遗漏**:
- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:23-27 - `MGPipeApplierIsUnbarrieredApply()`, a FIFTH member of A3-16's `if (Transport == Monolith) return false; if (!OnApplyThread()) return false;` family, in the same file the auditor opened and 6 lines above the row's own first cite. It is the only member of that family CALLED FROM THE BACKEND: MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2608 guards the Fatal{RoleViolation,"MGPipeSlots"} at :2609-2614 for a draw carrying client-memory vertex arrays applied unbarriered - 'the bytes below are the application's and are moving'. Declared MobileGL/MG_Impl/Pipe/SlotAllocator.h:178. Force-Monolith in the child disarms it at its first conjunct; a ServerMain that does not publish g_applyThreadKey disarms it at its second. It is also a NINTH MG_Impl symbol the backend needs at link time, which falsifies A3-9's and doc_drift #2's 'eight'.
- MobileGL/MG_Remote/Server/ServerLoop.cpp:667 (`if (OnApplyThread()) return ApplySurfaceControlFrame(frame);`) and :735 (same, with hook/user) - two `OnApplyThread()` sites that are not guards at all but the re-entrancy arms of RunSurfaceControlFrame. With the key unpublished (a ServerMain that drives the applier without ServerLoop::Start) these answer FALSE on the thread that IS the applier, the frame goes into the one-slot control channel and waits on m_controlDone 'for an answer only THIS thread could ever give' - the hang described in as many words at ServerLoop.cpp:517-527, which also says the integration lane 'would catch that as a hang, not as a failed assertion'. This is the strongest concrete case for c6_ask #6 and the item never names it: it is a deadlock, not a disarmed guard.
- MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:112-126 - `g_textureLegacyArmScopeDepth` is a NON-thread_local process global (:112, and the comment at :103-111 argues that is safe precisely because OnApplyThread() gates both ends), and MGPipeTextureLegacyArmScope's constructor counts only `if (m_counted)` where `m_counted = ServerLoop::OnApplyThread()` (:115-118). The Fatal at :60 and its named Magma exemption at :59 read the same predicate, so A3-16's recommendation to rewrite :56 as `MGPipeServerArm()` (Role==Server on EVERY server thread) WITHOUT also rewriting :116 silently kills the declared texture-sync exemption and converts it into a spurious Fatal on every non-apply server thread. A3-16 asserts the rewrite 'FIXES both halves' and never opens the scope.
- MobileGL/MG_Remote/Client/ClientSession.cpp:123 (`thread_local Bool g_roleInsideApplier = false;`) and :1299-1302 (`ApplyThreadIsInsideApplier()` returning g_roleInsideApplier under MGPipeRoleSplitActive(), the atomic otherwise) - A3-18 describes only the process atomic at :119. The comment at :120-122 is P5f's half-landed answer to this row and should be quoted in it.
- MobileGL/MG_Remote/Client/ClientSession.cpp:1357 and :1376 - two further `Server::ServerLoop::OnApplyThread()` guards inside the very function A3-18 opens, both omitted from c6_ask #6's 'nine guards' list. Full census at this head: 14 non-comment OnApplyThread() call sites outside the definition at ServerLoop.h:265-271 - Managers.cpp:230, Managers.cpp:2868, SlotAllocator.cpp:25/:31/:50, PipeApply.cpp:1355, ClientSession.cpp:1357/:1376, PersistentMapTracker.cpp:535, WireTables.cpp:87, ServerLoop.cpp:667/:735, MipmapStorage.cpp:56/:116. Not nine.
- MobileGL/MG_Remote/Transport/ITransport.h:40-44 - `enum class TransportRole { Server = 1 /* accepts the client connection */, Client = 2 /* connects to the server endpoint */, InProcess = 3 }`, consumed via ITransport::Role() (:124, implemented InProcessTransport.h:62, tested ServerSession.cpp:715/:733) - and MobileGL/MG_Remote/Transport/RoleMemory.h:42-46 `enum class MemoryRole { Client, Server }` (used ClientSession.cpp:600/:1490, ServerSession.cpp:486/:806). The item's headline recommendation is 'a three-valued Role and not a boolean Dial', and the tree already carries two Client/Server role vocabularies, one of which ALREADY encodes the accept-vs-dial distinction the review wants split out. c6 will reuse, rename or collide with these and the audit gives it no warning.
- MobileGL/MG_Backend/DirectGLES/Managers.h:341, :406, :430, :436 and MobileGL/MG_Backend/DirectGLES/SlotTables.h:281, :398, :410, :476 - eight backend call sites of `MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread`, defined in MG_Impl at SlotAllocator.cpp:48; plus `MG_Pipe::MGPipeSlots()` at SlotTables.h:285 and :481, declared MG_Impl/Pipe/SlotAllocator.h:165. doc_drift #2 sizes the MG_Impl link debt as 'eight undefined symbols reachable from the backend's own MGB_CTX reads'; the real surface is larger and is not MGB_CTX-shaped at all. MG_Backend also includes MG_Impl headers at 12 places (MobileGL/MG_Backend/DirectGLES/SlotTables.h:15, Managers.h:25, DirectGLES.cpp:38, DirectVulkan/Renderer/VulkanRenderer.cpp:20/:26/:27/:30/:48, UniformManager.cpp:16, VkRenderPassManager.cpp:11, SwapchainObject.cpp:11, DirectVulkan.cpp:15) and MG_State headers on 55 lines across 20+ files.
- MobileGL/MG_Backend/DirectGLES/Managers.h:708-723 and :1012-1019 - six process-lifetime function-static latches (FramebufferSubsystemEnabled / TextureResourceSubsystemEnabled / SamplerSubsystemEnabled / ProgramSubsystemEnabled / ResourceSubsystemEnabled / VertexInputSubsystemEnabled), resolved at Managers.cpp:2939 and :2966 off `MG_Config::Features.PipePush`. Same latch-at-first-use hazard A3-12/A3-13 name, on the OTHER config table: in the child these must equal the client's, the envp scrub A3-26 discusses touches only MOBILEGL_TRANSPORT and MOBILEGL_IPC_*, and the only thing carrying Features to the child is the handshake configBlob (ARCHITECTURE.md:489). The item examines only the Transport-keyed latches and so its `open` item on which env vars must be scrubbed/inherited/negotiated is missing the MOBILEGL_PIPE_* family entirely.
---
## a6.4 控制面
**小结**:All twelve Server* forwarders are value frames today - I opened every one (ServerLoop.cpp:1063-1178) and each packs a SurfaceControlFrame and posts it through RunSurfaceControlFrame; nothing carries a pointer. The contract's three "not on the wire" claims are two-thirds right: ServerSwapEGLBuffers has zero production callers (only MG_Test) and ServerInitWindowSurface has zero callers of any kind, so neither needs a socket form - but ServerInitCapabilities has a LIVE production caller on the first make-current of every surface (BackendObject_Remote.cpp:126, reached from BackendObject.cpp:341-347), and SurfaceOpCodec refuses to encode its kind (SurfaceOpCodec.cpp:119-122). "Answered by CapsSnapshot" describes the answer, not the request; under spawn there is no wire op by which a client can ask, and the first eglMakeCurrent fails. That is the one thing that makes "cp is TRANSPORT ONLY" false, and it is what c6 most needs from this item. The review's SurfaceReply.defaultFb claim is CONFIRMED at both ends (encoder hardcodes 0 at SurfaceOpCodec.cpp:184, decoder never reads it at :190-195, no test touches it). The one-slot mailbox needs no protocol change at all - it needs a second poster, because the apply thread never reads the control transport (ServerLoop.cpp:404-454) and PumpControlPlane drops SurfaceOp/SurfaceReply by name (ClientSession.cpp:1425-1432). Two further surprises: the EGL handles crossing the frame are ALREADY client-minted dense tokens (MG_State/EGLState/Core.h:212-215, counters at :247-250), so the token work P6-CONTRACT-DRAFT table 0 plans is already done; and the window-kind accept set for a window op is exactly {None, X11, Win32Hwnd} - I followed nativeToken all the way to eglCreateWindowSurface (DirectGLES.cpp:16084), which confirms the review's 2.1 hole end to end and adds a third member to it, WindowKind::None.
| A4-01 | p6-must-handle | ServerInitializeEGLDisplay(dpy, major*, minor*) - packs kind=InitializeDisplay + display token; the two out-pointers come back as frame.eglMajor/eglMinor. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::InitializeDisplay(1), reply carries eglMajor/eglMinor (already in SurfaceReply). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1063 (ServerInitializeEGLDisplay); dispatch at :902-913 (ApplySurfaceControlFrame); client caller MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:206 | Nothing in the payload. The forwarder itself calls ServerLoopInstance() with no role branch - see A4-13. | Detail::SurfaceControlFrameHasOnlyValues() static_assert (SurfaceControlFrame.h:118); wire-kind alignment static_assert | cp | |
| A4-02 | p6-must-handle | ServerCreateEGLWindowSurface(surface, WindowHandle&) - packs kind=CreateWindowSurface, surface token, and PackWindowHandle (windowBackend/nativeToken/width/height). VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::CreateWindowSurface(2) - but every production windowKind is refused at the server (A4-23/A4-24). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1078 (ServerCreateEGLWindowSurface); pack at :853-858; dispatch :914-921 | handle.Handle is a client-process value. Android dies by name at the decoder; X11/Win32 do NOT (A4-24). P6 is offscreen only, so this op should never legitimately cross in P6. | SurfaceWireError::AndroidNativeWindowArrived / MetalLayerArrived -> Fatal at SurfaceOpCodec.cpp:201-217 | cp | |
| A4-03 | already-handled | ServerResizeEGLWindowSurface(surface, w, h) - packs kind=ResizeWindowSurface + surface + width/height. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::ResizeWindowSurface(4), pure scalars. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1087; dispatch :922-926; client caller BackendObject_Remote.cpp:230 | nothing - it is four scalars | none needed | cp | |
| A4-04 | already-handled | ServerCreateEGLPbufferSurface(surface, w, h) - packs kind=CreatePbufferSurface + surface + width/height; the encoder stamps windowKind=Pbuffer from the op, and the decoder never reads windowKind for this kind. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::CreatePbufferSurface(3). THIS IS P6's ONLY WORKING SURFACE PATH. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1097; encoder windowKind at SurfaceOpCodec.cpp:124-126; decoder skips it because OpNeedsAWindowBackend is false (SurfaceOpCodec.cpp:55-57, :156) | nothing. This is the op P6's exit gate rides on. | none needed | cp | |
| A4-05 | p6-must-handle | ServerMakeEGLCurrent(dpy, draw, read, ctx) - ONE forwarder, TWO kinds: it self-classifies the three-NO_* release shape into SurfaceControlOp::ReleaseCurrent and everything else into MakeCurrent. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::MakeCurrent(6) / ReleaseCurrent(7); the four-tuple needs readSurface+context, which fc already appended. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1107-1121 (classification at :1112-1114); dispatch :935-983; wire fields protocol.fbs:208-209 | nothing in the payload. But the MakeCurrent arm republishes CapsSnapshot from inside the dispatch (:959-969) - see A4-30 for the socket ordering that creates. | static_asserts pin MakeCurrent=6/ReleaseCurrent=7 (SurfaceOpCodec.cpp:39-42); ID-67 republish counter (ServerLoop.h:311) | cp | |
| A4-06 | already-handled | ServerSwapEGLBuffers(dpy, draw) - packs kind=SwapBuffersInprocOnly. VALUE FRAME today. INPROC-ONLY. Socket form: NONE, and none is needed: it has ZERO production callers. The only callers in the tree are MG_Test/Wire/ServerLoopTest.cpp:1674 and :1693 (the N-3 recreate control, which needs a swap that is not a record). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1123; dispatch :1032-1038; the reason at Client/BackendObject_Remote.cpp:279-288 | nothing. Verified: grep over MobileGL/ finds no non-test caller. | SurfaceControlOpHasWireKind returns false (SurfaceControlFrame.cpp:47-48) -> EncodeSurfaceOpFrame answers InprocOnlyOpOn | cp | |
| A4-07 | p6-must-handle | ServerSetEGLSwapInterval(interval) - packs kind=SetSwapInterval + swapInterval; returns void, so the rc is discarded at the call site. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::SetSwapInterval(8), one int. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1132 (note the `(void)` at :1136); dispatch :984-987; client caller BackendObject_Remote.cpp:297 | nothing in the payload, but the discarded rc means a client whose server died sees no error at all from eglSwapInterval - the void forwarders drop MOBILEGL_ERR_NOT_INITIALIZED on the floor. | none - `(void)ServerLoopInstance().RunSurfaceControlFrame(frame)` | cp | |
| A4-09 | p6-must-handle | ServerReleaseEGLResources() - packs kind=ReleaseResources, no arguments; void, rc discarded. BLOCKING BY CONTRACT. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::ReleaseResources(9). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1146-1157 (the contract note at :1147-1153); dispatch :1000-1014; client caller BackendObject_Remote.cpp:309 | The two posters split across two PROCESSES. In the server process the inline arm still works. In the client process the call must become a socket round trip that BLOCKS until the server's DestroyEGLContext has run, because MobileGL::Destroy() walks on the moment it returns (Init.cpp:68, cited at ServerLoop.h:489-493). A fire-and-forget socket send would reinstate scout-install 5.3's hazard across a process boundary. | OnApplyThread() re-entrancy arm (ServerLoop.cpp:667); Fatal{ApplyThreadNotRunning} (:690-696) | cp | **claim-false** — Keep the row and keep verdict p6-must-handle - the BLOCKING requirement is real and correctly argued (MG_Impl/EGLImpl/EGLImpl.cpp:328 calls ReleaseEGLResources and :336 walks straight into MobileGL::D |
| A4-10 | blocks-p6 | ServerInitCapabilities() - packs kind=InitCapabilitiesInprocOnly. VALUE FRAME today. Marked INPROC-ONLY. Socket form: THERE IS NONE, AND IT NEEDS ONE. This is the single claim in the contract that the code contradicts. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1159-1164; dispatch :1019-1031; LIVE client caller MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:126 (`if (!Server::ServerInitCapabilities())`), reached from MG_Backend/BackendObject.cpp:341-347 on the first make-current per surface | EncodeSurfaceOpFrame REFUSES this kind (SurfaceOpCodec.cpp:119-122 -> InprocOnlyOpOnTheWire), so the spawned client cannot ask. With no wire form the forwarder runs in the CLIENT process against an empty ServerLoop and returns MOBILEGL_ERR_NOT_INITIALIZED -> BackendObject_Remote::InitCapabilities returns false -> the base class's MakeEGLCurrent fails -> the FIRST eglMakeCurrent under spawn fails. Not an edge case: it is on the bring-up path of every surface. BackendObject_Remote.cpp:119-125 argues explicitly why the client may not assume ServerMakeEGLCurrent already asked (a surface can be replaced without a new make-current). | SurfaceControlOpHasWireKind=false (SurfaceControlFrame.cpp:47-48) - the guard is doing its job; the gap is that a live c | c6 | |
| A4-11 | already-handled | ServerInitWindowSurface() - packs kind=InitWindowSurfaceInprocOnly. VALUE FRAME today. INPROC-ONLY. Socket form: NONE needed - it has ZERO callers anywhere in the tree, tests included. The client-side virtual is a bare `return true`. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1166-1171; dispatch :1039-1044; the client-side no-op at Client/BackendObject_Remote.cpp:145-150 | nothing | SurfaceControlOpHasWireKind=false; SurfaceControlFrameTest.cpp:269-275 | cp | |
| A4-12 | p6-must-handle | ServerSetWindowHandle(WindowHandle&) - packs kind=SetWindowHandle + windowBackend/nativeToken/width/height; void, rc discarded. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::SetWindowHandle(10) - same windowKind refusal surface as CreateWindowSurface. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1173-1178; dispatch :1015-1018; client caller BackendObject_Remote.cpp:215 (posted BEFORE ServerCreateEGLWindowSurface) | It is a second full socket round trip for something BackendObject::ActivateEGLSurface already does for itself from the registered surface state (BackendObject.cpp:283, `SetWindowHandle(surfaceState->Window)`). Worse, the dispatch stores the decoded nativeToken as a raw pointer with no backend check when windowKind is None - see A4-25. | WindowBackendFromFrameValue range check (ServerLoop.cpp:864-873) - but it ACCEPTS Unknown(-1) | cp | |
| A4-13 | blocks-p6 | THE TWELVE FORWARDERS HAVE NO ROLE BRANCH. Every one of them ends in `ServerLoopInstance().RunSurfaceControlFrame(frame)` unconditionally. There is no `if (MG_Config::Transport == Spawn)`, no ClientSession lookup, nothing. This is the whole of cp's client-side work and no line of it exists. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1063-1178 (all twelve); the singleton at :810-814; the declarations and their 'the seam is twelve signatures' rationale at ServerLoop.h:495-522 | In the client process ServerLoopInstance() has never been Start()ed and m_backend is null, so PostSurfaceControlFrameWithCallerLock takes the `!m_running && !backendAlive` arm and returns MOBILEGL_ERR_NOT_INITIALIZED (ServerLoop.cpp:698-703) with NO LOG LINE. Every EGL call under spawn would fail as an unnamed false - the exact silent-fallback shape this project forbids. The Fatal{ApplyThreadNotRunning} next door (:690-696) does NOT fire, because it is gated on backendAlive. | none on the path that is actually taken; the named Fatal is on the other arm | cp | |
| A4-14 | already-handled | THE ONE-SLOT BLOCKING MAILBOX, protocol in full. Poster: takes m_callerMutex (serialises every non-apply-thread poster), takes m_controlMutex, reads m_running under that same lock, copies the frame into m_controlFrame, sets m_controlPending=true / m_controlFinished=false, publishes the release-store shadow m_controlPosted LAST, rings the CONSUMER doorbell (publish-then-ring), then waits on m_controlDone. Taker: the apply thread's PumpControlRequest does a fast negative on the shadow, takes the lock, copies the frame OUT, clears the shadow (when TAKEN, not when finished), runs ApplySurfaceControlFrame OUTSIDE the lock, then republishes frame+result under the lock, sets m_controlFinished, notify_all. Poster copies the reply half out of m_controlFrame. | MobileGL/MG_Remote/Server/ServerLoop.cpp:658-671 (RunSurfaceControlFrame), :673-727 (PostSurfaceControlFrameWithCallerLock), :542-582 (PumpControlRequest); members at ServerLoop.h:394-423; the park predicate at ServerLoop.cpp:398-402 | NOTHING IN THE PROTOCOL. The slot already carries a pure-value frame; the handshake, the shadow, the doorbell order and the C2 exit block are all address-space-local to the server. What changes is only WHO POSTS: today the app/GL thread, under spawn a server-side reader. The minimal cp shape is an mgl-srv-io thread that ReceiveFrame -> DecodeWireSurfaceOp -> ServerApplyWireSurfaceOp -> EncodeSurfaceReplyFrame -> SendFrame, and that thread simply becomes the poster this code already supports. The alternative - a fourth arming term in `ready` so the apply thread polls the socket itself - would be the only change that touches this protocol. | C2 exit block answers a stranded request NOT_INITIALIZED rather than leaving it blocking (ServerLoop.cpp:490-502); m_con | cp | |
| A4-15 | blocks-p6 | THE POSTER'S WAIT HAS NO DEADLINE. `m_controlDone.wait(lock, [this]{ return m_controlFinished; })` - an unbounded predicate wait. Compare the ring side, which uses kBarrierTimeoutMs and dies Fatal{BarrierTimeout} by name. | MobileGL/MG_Remote/Server/ServerLoop.cpp:724; the bounded counterpart at Client/ClientSession.cpp:1198-1214 (WaitForApplyToCatchUp, Fatal{BarrierTimeout}) | A SIGSTOP'd / Android-freezer'd server produces NO EOF and NO reply. The GL thread then blocks forever inside eglMakeCurrent with no diagnostic. P6-SPAWN-PLAN §4 names the frozen-server case but only for the record/death path; this is the wait that actually wedges a GL thread, and it is not in that table. | none | cp | **overstated** — Soften to: 'P6-SPAWN-PLAN §4's table already requires a bounded wait + named diagnostic for the freezer case, but its chosen mechanism - SocketDoorbell::m_dead latched on EOF (Transport/Doorbell.h:388 |
| A4-16 | blocks-p6 | THE APPLY THREAD NEVER READS THE CONTROL TRANSPORT. Its loop is PumpControlRequest / DrainRing / park, and its `ready` predicate has exactly three terms (stop, ControlIsPending, cmdHead != LocalTail) plus the eventRingFull veto. No socket, no ITransport, no PeekFrameSize. The server's ONLY ITransport reads in the whole tree are the one handshake receive and the sends. | MobileGL/MG_Remote/Server/ServerLoop.cpp:398-454 (the loop and `ready`); the server's only transport reads/sends at Server/ServerSession.cpp:450 (handshake), :538 (Welcome), :653 (PublishCapsSnapshot) | A SurfaceOp arriving on the socket is read by nobody. There is no server-side control reader after the handshake. cp must add one (mgl-srv-io) or add the socket to the park predicate. This is the largest single piece of cp's actual work and the plan's 'cp 只剩传输' does not size it. | none - there is nothing to go red | cp | |
| A4-17 | blocks-p6 | THE CLIENT'S CONTROL PUMP DROPS SurfaceOp AND SurfaceReply BY NAME. PumpControlPlane consumes only CtrlMsg::CapsSnapshot; anything else is logged at WARN and `continue`d. The comment names SurfaceOp/SurfaceReply as 'P6's and P7's' precisely so a later phase does not discover the loop swallowing them - and that is now. | MobileGL/MG_Remote/Client/ClientSession.cpp:1406-1444, the drop at :1425-1432 | A SurfaceReply that lands while some other code path is pumping caps is WARNed and discarded, and the forwarder waiting for it never wakes. The control socket carries two interleaved streams (server-pushed CapsSnapshot, and replies correlated to requests) and needs a demultiplexer with a per-seq rendezvous. Today it has a single-consumer drain loop that understands one message type. | the MGLOG_W at :1429 - it will fire, which is better than silence, but the request still hangs (A4-15) | cp | |
| A4-18 | p6-must-handle | ClientSession holds BOTH ENDS of the transport and its start path is typed to the concrete InProcessTransport, not to ITransport. StartOverTransportPair takes two unique_ptr<InProcessTransport> and refuses if either is null. | MobileGL/MG_Remote/Client/ClientSession.cpp:474-496 (Start, the named non-InProcess refusal at :482-488) and :498-511 (StartOverTransportPair) | A spawn client owns ONE end. The signature must be generalised to ITransport (or a second entry point added), and the handshake stops being drivable by one thread - Hello and Welcome cross a real socket. ITransport already has the right shape (ITransport.h:46-128); nothing there needs to change. | the named refusal at :482-488 is exactly right and is what so/cp replace | so | |
| A4-19 | p6-must-handle | SurfaceReply.defaultFb IS DEAD AT BOTH ENDS - the review's claim CONFIRMED. The encoder passes a literal 0 as the DefaultFramebufferInfo offset (flatbuffers AddOffset skips a null offset, so the field is ABSENT in the buffer, not zero-valued); the decoder writes only seq/ok/eglMajor/eglMinor and never touches it; no test in the tree reads it. | encoder MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:181-188 (the `/*defaultFb=*/0` at :184); decoder :190-195; schema MobileGL/MG_Remote/Protocol/protocol.fbs:212-218; generated accessor protocol_generated.h:1225-1226; the round-trip test that covers everything BUT it, MG_Test/Wire/SurfaceControlFrameTest.cpp:239-261 | Nothing breaks - but the slot is a second truth source waiting to be revived by someone who reads the schema and not the code. | none; nothing asserts it stays empty | c6 | |
| A4-20 | p6-must-handle | SurfaceReply CARRIES NO MobileGLResult. The frame's reply half is {ok, eglMajor, eglMinor}; ServerApplyWireSurfaceOp RETURNS the rc but the wire has nowhere to put it. So 'the apply thread is gone' (NOT_INITIALIZED) and 'the driver said no' (ok=false) are the same byte on the wire. | schema MobileGL/MG_Remote/Protocol/protocol.fbs:212-218; encoder MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:183-184; the rc that is dropped, :219-221; the inproc collapse the forwarders do, ServerLoop.cpp:1075 (`return rc == MOBILEGL_OK && frame.ok`) | A client cannot tell a server that died mid-op from a legitimate refusal, so it cannot decide between arming the device-lost latch and returning EGL_FALSE. The test that exercises this collapse is MG_Test/Wire/ServerLoopTest.cpp:474-495: rc==NOT_INITIALIZED, wireReply->ok()==false, and the reply cannot say which. | none | c6 | |
| A4-21 | p6-must-handle | A seq==0 SurfaceOp IS SILENTLY RENUMBERED, not refused. RunSurfaceControlFrame mints a local seq when frame.seq is 0, and its own comment says a wire request 'is already numbered by its client and MUST retain that number' - but nothing enforces that the wire request actually had one. The encoder copies frame.seq verbatim and mints nothing. | MobileGL/MG_Remote/Server/ServerLoop.cpp:659-662; the encoder's verbatim copy at Protocol/SurfaceOpCodec.cpp:133-135 | Under spawn the CLIENT must mint, because the reply is correlated by seq. A client that forgets gets a server-minted number it has never seen - an uncorrelatable reply, silently. This is the same shape as Fatal{ProtocolCorruption}: a value the peer could not have written. | none - the renumbering is unconditional | c6 | |
| A4-22 | p6-must-handle | DecodeWireSurfaceReply DOES NOT CORRELATE. It overwrites seq/ok/eglMajor/eglMinor into whatever frame it is handed and returns void - no comparison against the request's seq, no error channel at all. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:190-195; header contract at SurfaceOpCodec.h:87 | The correlation has to live somewhere. If it lives in the pump rather than here, say so in the contract; if it lives here, this signature has to grow a SurfaceWireError like its SurfaceOp twin. | none | c6 | |
| A4-23 | blocks-p6 | THE WINDOW-KIND ACCEPT SET, EXACTLY. windowKind is only consulted for the two ops OpNeedsAWindowBackend answers true for: CreateWindowSurface and SetWindowHandle. For those: ACCEPTED = {None(0) -> WindowBackend::Unknown, X11(2) -> X11, Win32Hwnd(3) -> Win32}. REFUSED = {AndroidNativeWindow(1) -> AndroidNativeWindowArrived, MetalLayer(6) -> MetalLayerArrived, Surfaceless(4) and Pbuffer(5) -> WindowKindNamesNoBackend, any value 7..255 -> UnknownWindowKind}. All four refusals reach std::abort at the wire entry, the first two with UnmigratedSurface@P12 messages and the rest with Fatal{ProtocolCorruption,"SurfaceOp"}. For every OTHER op windowKind is ignored entirely, including CreatePbufferSurface, whose encoder stamps Pbuffer that nobody reads. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:55-57 (OpNeedsAWindowBackend), :104-114 (WindowBackendForWireWindowKind), :156-176 (the decode arm), :197-222 (the Fatals); schema protocol.fbs:181-192 | X11 and Win32Hwnd are ACCEPTED, and that is the review's 2.1 hole - I confirm it and add a third member: WindowKind::None is accepted too (:110), and the decode still copies nativeToken through (:175) even though the backend tag says Unknown. | MG_Test/Wire/SurfaceControlFrameTest.cpp:295 (Surfaceless), :308 (Android), :327/:350/:364 (the aborts). NOTHING tests X | c6 | **claim-false** — Replace the guard sentence with: 'X11, Win32Hwnd and None are each pinned as ACCEPTED, both directions, at SurfaceControlFrameTest.cpp:162-174, and X11 + a nonzero nativeToken is pinned as a successfu |
| A4-24 | blocks-p6 | nativeToken FOLLOWED ALL THE WAY INTO THE DRIVER, as asked. For an accepted windowKind: op.nativeToken() -> frame.nativeToken -> UnpackWindowHandle's HandleFromToken<void*> (an unconditional reinterpret_cast) -> WindowHandle::Handle -> RegisterEGLWindowSurface stores it in EGLSurfaceState::Window -> ActivateEGLSurface calls SetWindowHandle(surfaceState->Window) -> BackendObject_DirectGLES::InitWindowSurface does `reinterpret_cast<NativeWindowType>(m_windowHandle.Handle)` -> DirectGLES::InitWindowSurface calls `eglCreateWindowSurface(g_Display, g_Config, window, nullptr)`. There is no validation anywhere on that chain except the Backend-tag range check and the `!handle.Handle` null test. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:174-175 -> Server/ServerLoop.cpp:875-882 (UnpackWindowHandle) -> MG_Backend/BackendObject.cpp:227-238 (RegisterEGLWindowSurface) -> :282-283 (ActivateEGLSurface) -> DirectGLES/BackendObject_DirectGLES.cpp:899-907 -> DirectGLES/DirectGLES.cpp:16079-16095 (the eglCreateWindowSurface at :16084) | On a Linux/Windows build an X11 XID or an HWND reaches eglCreateWindowSurface in the SERVER process. An XID and an HWND happen to be system-scoped and might even work, which is worse than crashing: it makes the refusal look unnecessary while a guest with a private window system hands the server a wild value. This is the review's 2.1, confirmed end to end at this head. | BackendObject.cpp:227's `handle.Backend == Unknown // !handle.Handle` - a soft `return false` with an MGLOG_E, not a nam | c6 | |
| A4-25 | p6-must-handle | WindowKind::None ON A WINDOW OP is accepted, maps to WindowBackend::Unknown(-1), and the raw nativeToken is copied through anyway. WindowBackendFromFrameValue's range check passes -1 (its lower bound IS Unknown), so UnpackWindowHandle builds {Unknown, <token-as-pointer>, w, h}. CreateWindowSurface then fails softly at BackendObject.cpp:227; SetWindowHandle does not fail at all - it just STORES the foreign pointer. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:110 (None -> Unknown), :174-175 (token copied regardless); MobileGL/MG_Remote/Server/ServerLoop.cpp:864-873 (the range check that admits -1), :875-882; MG_Backend/BackendObject.cpp:227-230 and :473-476 | A corrupt or hostile SurfaceOp with windowKind=None and a nonzero nativeToken gets a client-process pointer STORED in the server's m_windowHandle with no refusal of any kind. It is later overwritten by ActivateEGLSurface, so it is latent rather than immediately fatal - but 'latent' is exactly what the whitelist exists to remove. | none for SetWindowHandle; a soft false for CreateWindowSurface | c6 | |
| A4-26 | already-handled | THE EGL HANDLES ARE ALREADY CLIENT-MINTED DENSE TOKENS. MG_State fabricates every EGLDisplay/EGLConfig/EGLSurface/EGLContext as `EncodeHandle<T>(m_next*Handle++)` over counters that all start at 1; they are never driver handles. The server's BackendObject uses them purely as std::map keys and identity comparisons; the REAL EGL objects are DirectGLES' process globals g_Display/g_Surface/g_Context, created from the native window and never named on the wire. | MobileGL/MG_State/EGLState/Core.h:212-215 (EncodeHandle), :247-250 (the four counters, each = 1); mint sites Core.cpp:143 (display), :163 (config), :687 (context), :830/:868/:905/:945/:977/:1010 (surfaces); server-side use as map keys MG_Backend/BackendObject.cpp:198, :232, :265-268, :324-335; the real objects at DirectGLES/DirectGLES.cpp:16031-16056, :16084 | NOTHING. This is the best news in the item: P6-CONTRACT-DRAFT table 0's DisplayToken/SurfaceToken/ContextToken ('minted by the client, dense from 1') is a description of what MG_State already does, not work to do. The server needs no token->native map, because it never had native handles to map to. | the static_asserts in TokenFromHandle/HandleFromToken that the type is a pointer | c6 | |
| A4-27 | already-handled | ForgetCurrentTuple AND ITS N-3 RULE LIVE INSIDE THE DISPATCH, so the frame path preserves them by construction. Six call sites: ApplyThreadMain's exit (after ~backend), Stop()'s never-started arm, and four dispatch arms - CreateWindowSurface on success, CreatePbufferSurface on success, ReleaseSurface via ForgetCurrentTupleIfItNames, and ReleaseResources. The rule itself: forget after any event that may have destroyed the native context the held tuple names, because EGL handles are recycled (on this host they are literally 0x1). | MobileGL/MG_Remote/Server/ServerLoop.cpp:325-336 (the two functions); call sites :478, :764, :919, :933, :996, :1012; the rule stated at ServerLoop.h:325-337; the red-once control named at ServerLoop.cpp:1007-1011 | NOTHING. Because the calls are on the dispatch side of the seam, an io-thread poster changes nothing about them. P6-CONTRACT-DRAFT §4.4 and P6-SPAWN-PLAN:83 both claim this; I verified it at all six sites and the claim holds. | ServerLoopTest's C7/N-3 recreate control reads NativeBindCount()==1 where 2 is required if :1012 is deleted (stated at S | cp | |
| A4-28 | already-handled | WHICH CONTROL OPS NEED A GL/EGL CONTEXT TO COMPUTE - all of them are on the SERVER side, on the apply thread, which already owns the context for life. NEED IT: CreateWindowSurface / CreatePbufferSurface (they CREATE the context via InitDisplayAndContext), MakeCurrent (native eglMakeCurrent + the base class's InitCapabilities GL queries), SetSwapInterval (eglSwapInterval needs a current context - DirectGLES defers when there is none), ReleaseSurface (may reach DestroyEGLContext), ReleaseResources (DestroyEGLContext), and the three inproc-only kinds. DO NOT: InitializeDisplay (two constants), ResizeWindowSurface (bookkeeping only, not overridden by DirectGLES), ReleaseCurrent (the ClientRelease arm records and does not forward), SetWindowHandle (a struct store). | MG_Backend/BackendObject.cpp:182-187 (InitializeDisplay constants), :196-210 (Resize, bookkeeping); ServerLoop.cpp:282-293 (ClientRelease, no native call); BackendObject.cpp:473-476 (SetWindowHandle); DirectGLES/DirectGLES.cpp:16031-16056 + :16079-16095 (context creation), :16065-16077 (swap interval), :16178-16185 (MakeCurrent); BackendObject_DirectGLES.h:31-50 (the override list, which is how I know Resize and SetSwapInterval are not overridden) | nothing - the split is already drawn in the right place. Under spawn the apply thread stays the context owner in its own process, which is, in ServerLoop.h's words, 'exactly the shape P6's spawned server inherits'. | the 32 existing context-owner guards | cp | |
| A4-29 | already-handled | NOTHING IN THE CLIENT'S CONTROL PATH NEEDS A GL CONTEXT. The nine EGL virtuals on BackendObject_Remote do exactly three things besides forwarding: a run-ahead wait on a shm watermark, a caps-mirror refresh, and a SEG_EVENT drain that writes MG_State texture shadows. No GL call, no driver entry point. | MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:190-194 (WaitForApplyBeforeEglForwarder), :83-92 (RefreshFormatCapabilities, reads CapsMirror), :224/:233/:242/:272 (DrainPublishedEvents); the event consumer at Client/ClientSession.cpp:225-257, which only calls MG_State AllocateStorage/SetInternalFormat | nothing. The answer to the item's last question is: no. | CONTRACT-P5C rule E / the RoleViolation guards | cp | |
| A4-30 | blocks-p6 | CapsSnapshot IS PUSHED FROM INSIDE THE DISPATCH, on the apply thread, mid-op. The MakeCurrent arm and the InitCapabilities arm both call ServerSession::PublishCapsSnapshot(), which builds a FlatBuffer and calls SendEnvelope on the same ITransport the reply will use. | MobileGL/MG_Remote/Server/ServerLoop.cpp:959-969 (MakeCurrent arm) and :1025-1030 (InitCapabilities arm); Server/ServerSession.cpp:609-654 (the send at :653); ITransport's threading note at Transport/ITransport.h:27-29 ('callers serialize sends') | Two consequences. (1) ORDERING: the snapshot must reach the client BEFORE the reply that its adoption depends on, because the client pumps and adopts right after the RPC returns (BackendObject_Remote.cpp:268-273). Same-socket FIFO gives that for free ONLY if the same thread sends both - i.e. if the io thread does not send the reply while the apply thread is still sending the snapshot. (2) SERIALISATION: under spawn the server has two potential senders (the apply thread's PublishCapsSnapshot and the io thread's SurfaceReply) on an interface that is explicitly not internally synchronised for send. | none - ITransport documents the requirement but does not enforce it | cp | |
| A4-31 | no-action | MGPipeServerSetContextLive, called from five sites in the control dispatch, writes a PROCESS-LOCAL static g_serverContextLive. Both the setter and the only reader are server-side: MGPipeServerContextIsLive is read from PipeInputs::IsLive, which is reached only through MGB_CTX_LIVE, and every MGB_CTX_LIVE use site is under MG_Backend/. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:305-312; writers at Server/ServerLoop.cpp:290, :295, :314, :475, :761, :1006; reader chain MG_Impl/Pipe/PipeFill.cpp:2135-2141 -> MG_Pipe/PipeInputsSwitch.h:20 -> MG_Backend/DirectGLES/Managers.cpp:7304 etc. | NOTHING. I checked specifically because the control dispatch touches it: there is no client-side reader of MGB_CTX_LIVE anywhere under MG_Impl or MG_State, so the static stays inside the server process and keeps meaning what it means. (Flagging for the integrator in case a6.2's statics item counts it - it is server-local and needs no carrier.) | purity gate C's ban on pGLContext outside MG_State/MG_Impl keeps MGB_CTX_LIVE server-side | cp | |
| A4-32 | p6-must-handle | ServerSetWindowHandle IS A REDUNDANT BLOCKING ROUND TRIP. The client posts it immediately before ServerCreateEGLWindowSurface, but the server's own ActivateEGLSurface sets the window handle from the surface it just registered. | client MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:212-216; the server's own set at MG_Backend/BackendObject.cpp:282-283 (`SetWindowHandle(surfaceState->Window)`), fed from RegisterEGLWindowSurface's store at :232-238 | Two socket round trips instead of one, each with the reply wait of A4-15, at exactly the moment the plan's §6.8 wants to measure socket-doorbell cost against inproc condvar. Also doubles the surface at which A4-25's unvalidated SetWindowHandle store is reachable. | none | cp | |
| A4-33 | already-handled | THE TEST PROBE'S RAW FUNCTION POINTER IS A MEMBER, NOT SLOT CONTENT, and ProbeForTesting has no wire kind - so the one remaining pointer in the control channel cannot cross. Worth recording because P5c row G4 was exactly 'a function pointer in the mailbox'. | MobileGL/MG_Remote/Server/ServerLoop.h:228-235 and :424-428 (m_controlProbeHook/m_controlProbeUser as atomics beside the slot); SurfaceControlFrame.cpp:47-48 (no wire kind); the probe dispatch at ServerLoop.cpp:892-895 | nothing. The pointer never leaves the process it was taken in, and the codec refuses the kind. | SurfaceControlOpHasWireKind=false; SurfaceControlFrameTest.cpp:269-275 covers all four inproc-only kinds | cp | |
| A4-34 | p6-must-handle | THE CONTROL PUMP'S LOOP BOUND IS PeekFrameSize()==0, which means something different on a stream socket than on a message queue. On InProcessTransport a message is whole or absent; on a socketpair a partially reassembled frame also answers 0. | MobileGL/MG_Remote/Client/ClientSession.cpp:1415-1418; Transport/Framing.h:9-28 and :79-80 (FrameReader is a byte-stream reassembler, explicitly 'it never assumes a read() returned a whole frame'); ITransport.h:83-85 | A reply-wait loop that breaks on PeekFrameSize()==0 would spin out of a half-arrived frame and report no reply. The pump's bound and the reply wait need different shapes: the pump stays non-blocking, the reply wait must block with a deadline (A4-15). | Framing.h latches PROTOCOL_MISMATCH on a violated frame, so corruption is named - but a short read is not corruption | so | |
**复审另行指出的遗漏**:
- MobileGL/MG_Remote/Client/ClientSession.cpp:704 - `const MobileGLResult running = Server::ServerLoopInstance().Start(server);`. The CLIENT's Start() is what starts the apply thread; the comment at :700-703 says so ('Under `inproc` the client is what starts the server role, which is why this call is here rather than in some server-side main'). Under spawn this line IS the server-side main that does not exist. It is the same no-role-branch shape as A4-13 and it is not in a6.4 at all; it is also absent from P6-ENDSTATE-REVIEW.md:264's sm list (which names :534-539, :577, :599-600, :758 but not this).
- MobileGL/MG_Remote/Client/ClientSession.cpp:802 - `Server::ServerLoopInstance().Stop();` inside the client's teardown, between transport->Shutdown() (:797) and the emitter release (:808-813). The bounded join, the private-backend destruction and the ordering argument at :800-805 all live on the client's thread today. Same gap, same omission from both a6.4 and the review's list.
- MobileGL/MG_Remote/Client/ClientSession.cpp:534-535, :577, :582, :599-600, :758, :814 - the remaining direct reaches from client code into the server singletons: `Server::ServerSessionInstance()` + `server.Accept(*m_serverTransport)` (:534-535), the Welcome cross-check that reads `server.Shm().AnnouncedSize(slot)` (:577, :582), `m_shm.AttachInProcess(server.Shm(), ...)` (:599-600), and two `ServerSessionInstance().Close()` (:758, :814). A4-18 covers only the InProcessTransport typing of StartOverTransportPair and does not enumerate these even to assign them to sm/so. The review has four of the six; the audit should have closed the set with file:line so c6 can see the whole seam at once.
- MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:156-176 - `nativeToken` is SILENTLY DROPPED for the eight non-window ops. `decoded.nativeToken = op.nativeToken()` (:175) is inside the `if (OpNeedsAWindowBackend(kind))` block opened at :156, so a wire SurfaceOp with kind=ReleaseSurface (or MakeCurrent, or ReleaseResources) carrying nativeToken=0xDEADBEEF decodes to frame.nativeToken == 0 with no log, no SurfaceWireError and no Fatal. A4-23 caught the windowKind half of this ('for every OTHER op windowKind is ignored entirely') but not the token half - and nativeToken is the field that actually carries a foreign address. By the project's own rule (and by A4-21's framing of seq==0), a value the peer could not legitimately have written is Fatal{ProtocolCorruption}, not a silent zero. c6's Rule G should cover both fields, not just the kind.
- MobileGL/Config.h:443-449 and :455 - c6_asks item 2 offers `MG_Config::Transport == Spawn` as one of three candidate role predicates. TransportMode::Spawn = 2 does exist (:446) and Transport is a runtime `extern TransportMode` under MOBILEGL_BUILD_DISAGGREGATED (:455, constexpr Monolith only in the non-disaggregated build at :566), so the predicate is mechanically viable - but P6-ENDSTATE-REVIEW.md:264 has ALREADY ruled against the Transport dial as the role predicate: it requires two orthogonal axes `Role{Monolith, Client, Server}` x `Dial{No, Fork, Connect}` and names forcing the child to Monolith as the trap ('后者会选中 frontend glue,正是 CONTRACT §3.1 点名的陷阱'). The review is listed as key reading and is what AMENDED a6, so the ask should have been framed as 'confirm the review's MGPipeServerArm() against these twelve sites', not as an open three-way choice.
- MobileGL/MG_Backend/BackendObject.h:616-620 - `enum class SurfaceKind { None, Window, Pbuffer }`. There is no surfaceless surface kind anywhere in BackendObject, and `WindowKind::Surfaceless` (protocol.fbs:186) is refused by the decoder (SurfaceOpCodec.cpp:168-172 -> WindowKindNamesNoBackend, pinned at SurfaceControlFrameTest.cpp:181, :295). A4-04 states correctly that pbuffer is 'P6's ONLY WORKING SURFACE PATH' but never draws the doc consequence: P6-SPAWN-PLAN.md:40 and P6-CONTRACT-DRAFT.md:107 both promise 'pbuffer / surfaceless / 离屏', and surfaceless has no backend implementation and no wire op at this head. That belongs in doc_drift beside the other two P6-SPAWN-PLAN entries.
- In-code citation drift in the exact comments a6.4 quotes, none of it flagged: (a) MobileGL/MG_Remote/Server/ServerLoop.h:491 and MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:307 cite 'MobileGL/Init.cpp:68' for pActiveBackendObject.reset(); at this head that call is MobileGL/Init.cpp:77 and :68 is a comment about the live-sync registry. (b) MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:257 cites 'ServerLoop.cpp:613-628' for the caps republish; the republish is ServerLoop.cpp:959-969. (c) MobileGL/MG_Remote/Server/ServerLoop.cpp:1148 and ServerLoop.h:489 cite 'EGLImpl.cpp:326' for the ReleaseEGLResources call; it is MG_Impl/EGLImpl/EGLImpl.cpp:328. A4-09 reproduces (a) and (c) verbatim as if they resolved.
- P6-CONTRACT-DRAFT.md:46 cites `protocol.fbs:234-242` for 'FatalCode::ServerCrashed / DeviceLost ... Already allocated'. At this head `enum FatalCode` is protocol.fbs:248-256 (ServerCrashed = 5 at :254, DeviceLost = 4 at :253); lines 234-242 are `enum AuxRequestKind` plus the `table AuxRequest` header. Same table 0 the audit combed for drift (it caught :43's protocol.fbs:186-196 and :44's correct 166-179) - this row was skipped.
---
## a6.6 Transport 站点分类
**小结**:The 238-site census is CONFIRMED exactly, but only with the integrator's own counting rule: 238 = grep-lines matching `MG_Config::Transport` (any suffix, so `TransportMode` and `TransportEndpoint` count) in non-test dirs, distributed 155 MG_Backend / 35 MG_Impl / 21 MG_Remote / 12 MG_Pipe / 8 MG_State / 7 Config. Twelve of the 238 are COMMENT lines, so there are 226 live code lines; counting occurrences of the bare symbol instead gives 228. The eight MG_State sites are NOT the sample the plan thinks they are: two of the eight (BufferObject.cpp:60, BufferObject.h:194) are comments, five of the remaining six are single-question `SplitActive` tests of the R-6 / third-state shape, and only ONE (MipmapStorage.cpp:55) literally asks both questions in one function. The "asks both questions" shape is not MG_State-local at all - it appears at 18 sites across MG_Impl/Pipe/SlotAllocator.cpp, MG_Impl/Pipe/PipeFill.cpp, MG_Pipe/PipeApply.cpp, MG_Backend/DirectGLES/Managers.cpp and MG_Remote/Client, and it already has two spellings in the tree (`Transport!=Monolith && !RunsAsTheServerRole()` for the client arm, `Transport!=Monolith && OnApplyThread()` for the server refusal). The three proposed predicates DO cover everything I opened, but a fourth axis exists and it is not a predicate: WHEN the question is asked. Most sites ask per-draw; VulkanRenderer.cpp:14593/:14616 and StagedTextureStore.h:475 ask ONCE, during device creation or at first use, which means the spawn server's Role must be established before MG_Backend::Init() rather than at session start. The one thing c6 most needs from this: decide whether the spawn SERVER process carries `Transport != Monolith` or `Transport == Monolith`, because 148 backend code lines and both Fatal role guards flip together on that single bit, and a server process left at Monolith selects the frontend glue arm everywhere - which P6-ENDSTATE-REVIEW.md:264 already names as the trap but which nothing in the tree currently prevents.
| A6-S1 | no-action | Comment line documenting that PushIsArmed() is a read of MG_Config::Transport and OnServerRole() a relaxed load of the apply-thread key. NOT a code site. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:60 (comment inside RefuseLegacyBufferArmFromApplyThread) | Nothing at this line. But the comment's cost argument ("one relaxed load each") is what a rename has to preserve: if MGPipeSplitActive() stops being one load of a process global it regresses a per-draw guard that simpleperf measured at 2.66% self on the client thread (ServerLoop.h:90-96). | none (comment) | c6 | |
| A6-S2 | p6-must-handle | `if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;` - R-6's "second door": refuse the LEGACY backend persistent-map mint when a transport is live. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:308 (BufferObject::TryAdoptLargeStorage) | Nothing, if the client process keeps Transport != Monolith. This is a pure SplitActive question and it never asks who is running. In the SERVER process the function is unreachable (no frontend BufferObject exists there), so the answer is irrelevant on that side. | the R-6 decline is doubled at PipeApply.cpp:2287 and at PipeWireCodec.cpp:1636; PipeStats' persistent-map-push counter i | sm | |
| A6-S3 | p6-must-handle | `if (MG_Config::Transport != Monolith && atOffset == 0 && data.size >= m_size) { m_gpuWritePending = false; }` - the third state's only exit. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:559 (BufferObject::WritebackFromBackend) | Nothing structural - it is SplitActive only. One latent sharpness: the condition `data.size >= m_size` is how a whole-buffer writeback is recognised, and the SLICED readback path at :605-618 has to clear m_gpuWritePending by hand for exactly that reason. Under spawn the slice size (BufferWritebackSliceBytes) is driven by ring capacity, so more buffers will take the sliced path and the by-hand clear at :614 becomes the common case rather than the exception. | AwaitBufferWriteback's third-state Fatal (BufferObject.cpp:619-628, MG_Remote/Client/GpuWritePending.cpp) | sm | **line-does-not-resolve** — Sliced path :594-612; by-hand clear :612; whole-buffer emit :614; AwaitBufferWriteback call :620; no-route clear :627-629. Drop 'BufferObject.cpp:619-628' from the guard field and keep only MG_Remote/ |
| A6-S4 | p6-must-handle | `if (MG_Config::Transport != Monolith) { ... }` - the WHOLE split arm of SyncGpuWrites: emit readback (sliced or whole), then block in AwaitBufferWriteback. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:590 (BufferObject::SyncGpuWrites) | This is the one MG_State site whose failure mode is a HANG rather than a Fatal. AwaitBufferWriteback blocks on a reply slot. If the spawn SERVER process is configured with Transport != Monolith and any thread there reaches SyncGpuWrites on a frontend BufferObject (there should be none, but the guard that would catch it is the accessor guard, not this function), it emits a readback with no ClientSession and waits. Today inproc is protected only because the apply thread aborts earlier in the accessor. | indirect: RefuseLegacyBufferArmFromApplyThread on the accessors; BufferWritebackIsReachable's no-route clear at :627 | sm | |
| A6-S5 | p6-must-handle | `if (MG_Config::Transport != Monolith) return false;` - R-6's second door again, on the shader-storage residency path. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:879 (BufferObject::EnsureGpuResidentStorage) | nothing - pure SplitActive, one direction, and the fallback (stay shadow-backed) is the spawn-correct answer | same as A6-S2 | sm | |
| A6-S6 | p6-must-handle | `MG_Config::Transport == MG_Config::TransportMode::Monolith &&` as a CONJUNCT inside a multi-line map-persistent condition (not a statement of its own). | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:958 (BufferObject::AcquireMemoryRange, the legacy AcquirePersistentMap arm) | nothing semantically; it is the textual shape that resists a mechanical rewrite | same as A6-S2 | sm | |
| A6-S7 | no-action | Comment line: "Everything here is behind the build option AND behind `MG_Config::Transport != Monolith` at the call site". NOT a code site. | MobileGL/MG_State/GLState/BufferState/BufferObject.h:194 (the "P5 b1" block declaring PushMappedSpanBlock / NotePersistentMapStateChanged / HasLiveHostWritesForWire / HasOutstandingGpuWrite) | nothing at this line, but the comment is the thing c6 will read to decide which members are client-only, and it understates where the gate actually sits | none (comment) | c6 | **line-does-not-resolve** — Call sites are BufferObject.cpp:227 (ResizeShadow), :378 (ReleaseMemory), :833 (glMapBuffer arm), :923 (AcquireMemoryRange), :947 (the MOBILEGL_PIPE_PUSH persistent arm). The callee's own gate is :489 |
| A6-S8 | p6-must-handle | `if (MG_Config::Transport == Monolith) return;` immediately followed by `if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return;` then a named-scope exemption, then Fatal{RoleViolation, "texture-legacy-arm"}. THE two-question sample. | MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:55-70 (RefuseLegacyTextureArmFromApplyThread) | Both halves become structurally constant, in opposite directions, and that is the finding. In the spawn CLIENT process g_applyThreadKey is永 0 (ServerLoop.h:265-271: `key != 0 && key == CurrentThreadKey()`), so OnApplyThread() can never be true and the guard can never fire - a gate that cannot go red. In the spawn SERVER process there is no frontend TextureObjectMipmap to call it on, so it also cannot fire. The guard's whole value evaporates at exactly the moment spawn makes the violation impossible - which is fine as an outcome but must be a DECLARED retirement, not a silently dead abort. | itself; the pinned surface list in MG_Test/Wire/RemoteClientTest.cpp's RemoteGuards is what keeps the list honest | sm | **line-does-not-resolve** — RefuseLegacyTextureArmFromApplyThread :54-67; the Monolith test :55; the OnApplyThread test :56; the MGPipeTextureLegacyArmScope exemption :59; the Fatal :60-66. RefuseLegacyTextureShapeReadFromApplyT |
| A6-P1 | blocks-p6 | PATTERN: the backend body's server-arm test. 148 code lines (150 total minus 2 comments) across MG_Backend/DirectGLES (98 lines) and MG_Backend/DirectVulkan (52 lines). Predominant form is the bare `if (MG_Config::Transport != Monolith) {` / `... == Monolith) return;`. | Representatives opened: MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:315 (GetRawDepthFetchSampler), :442 (indirect command resolve), :876 (shader-storage GPU-write walk), :2483 (SyncClientSideVertexArraysForFetch), :8398-8417 (multi-draw indirect), :15600 (GetTexImage PBO); MobileGL/MG_Backend/DirectGLES/Managers.cpp:3289, :3482; MobileGL/MG_Backend/DirectGLES/Utils.cpp:55 (ActiveBackendFormatCaps), :2363; MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:1054; MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:685 (ApplyLineWidthState), :3162, :13148 | This is the headline. All 148 flip together on ONE bit in the server process. If c6 spells "the server does not dial" as `Transport = Monolith` (the trap P6-ENDSTATE-REVIEW.md:264 names), every one of these selects the frontend glue arm inside a process that has no frontend. The failure is NOT uniformly loud: DirectGLES.cpp:2483 would read a null GetBoundVertexArray() and `return true` silently; Managers.cpp:3482 would call `bufferObject->SyncPersistentMappedRange()` on a null pointer's guard and skip; VulkanRenderer.cpp:13148 would read MGB_CTX and hit MGP_INPUT_CHECK's poison Fatal by FIELD name, which names the field and not the wrong-arm decision. | partial and at the wrong altitude: MGPipeInputPoisonFatalForVerb names a field, not a role; no gate exists that says "th | c6 | |
| A6-P1a | later-stage | SUB-PATTERN of P1: role-INDEXED statics. `Array<RoleXfbState, 2> g_roleXfbState; Int g_activeXfbRole = -1;` with `const Int role = MG_Config::Transport != Monolith;` used as the subscript, plus four `const Bool server = ...` selectors that pick between a per-role member and the shared one. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1641-1656 (ActiveXfbState), :1645 (the subscript), :1662 (CurrentXfb), :1807-1809 (BindScatterCaptureBuffer), :1848-1850 (ScatterCapturedRecords), :2212 (BindTransformFeedback), :2257 (DeleteTransformFeedback) | Nothing breaks - but the whole construct is a same-address-space artifact. Under spawn each process has exactly one role and `g_roleXfbState[0]` is dead in the server, `[1]` dead in the client. Leaving it is harmless; collapsing it is the kind of cleanup that silently changes which key a test observes, so it should be a named decision rather than a drive-by. | none; g_activeXfbRole's NeedsBind reset is the only thing that notices a role change | cp | |
| A6-P1b | blocks-p6 | SUB-PATTERN of P1: bring-up-time, ONCE-per-process reads of Transport inside device/instance creation and inside a function-static initialiser. | MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:14593 (descriptorIndexingCore, inside CreateDevice's feature selection), :14616 (wireDepthResolveEnabled); MobileGL/MG_Remote/Server/StagedTextureStore.h:475 (ServerStagedTexture's `static StagedTextureStore& store = *new StagedTextureStore(Transport != Monolith);`); MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:19 (CurrentContextEpoch) | These decide BEFORE any session exists, so the spawn server's Role must be established at process entry - before MG_Backend::Init(), before VulkanRenderer's vkCreateDevice - not at ClientSession/ServerSession handshake time. Any design that derives ServerArm from "a session is attached" or "the apply thread is running" gets the wrong answer here, because on Magma the descriptor-indexing and depth-resolve feature set is baked into the VkDevice at this point and cannot be changed later. | none - a wrong answer here is a device created without the extensions the wire path needs, surfacing much later as a mis | c6 | **claim-false** — Drop ContextEpoch.h:19 from P1b and file it under A6-P1 (ordinary per-draw server-arm test). The bring-up-once family is then VulkanRenderer.cpp:14593, :14616, StagedTextureStore.h:475 and the missed |
| A6-P2 | p6-must-handle | PATTERN: the client-arm emit/decide test, single question. ~48 code lines: MG_Impl (35 lines, 1 of them comment) + MG_State (6 code lines, rows S2-S6) + MG_Remote/Client's own predicates. | MobileGL/MG_Impl/Pipe/PipeFill.cpp (19 lines); MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6079, :6543, :6572, :6832; MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp:1347, :1409; MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp:782; MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:1186; MobileGL/MG_Impl/Pipe/VertexInputEmit.h:389, :396; MobileGL/MG_Impl/Pipe/ResourceTracker.h:585, :704 | Nothing, in the client process. In the server process these TUs are linked (same libMobileGL.so) but should be unreachable. The two Fatals at ResourceTracker.h:585/:704 are the ones worth keeping: they fire on `Transport != Monolith` alone with no role term, so they stay able to go red in either process. | the two named Fatals above; MGPipeInputPoison for the fill side | sm | |
| A6-P2a | blocks-p6 | PATTERN: the TWO-QUESTION sites - `Transport != Monolith` conjoined with a role probe in the same function. 18 sites, in TWO spellings that mean opposite things: the CLIENT arm `Transport != Monolith && !RunsAsTheServerRole()` and the SERVER refusal `Transport != Monolith && OnApplyThread()`. | CLIENT arm: MobileGL/MG_Impl/Pipe/PipeFill.cpp:357 (ClientRunsAhead), :975 (respecify+subdata follow-up), :1114 (flush-range follow-up), :1255 (MGPipeClientFinish), :3597 (applier reset); MobileGL/MG_Remote/Client/GpuWritePending.cpp:226 (MGPipeStageChunkBytes). SERVER refusal: MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:24 (MGPipeApplierIsUnbarrieredApply), :30 (MGPipeRefuseAllocatorFromApplyThread), :49 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread); MobileGL/MG_Impl/Pipe/PipeFill.cpp:1205 (MGPipeDeferDestroyAndFreeIfOnApplyThread); MobileGL/MG_Pipe/PipeApply.cpp:1353 (MGPipeApplierReset's Fatal); MobileGL/MG_Backend/DirectGLES/Managers.cpp:229 (OnFrontendStateObjectDestroyed), :2867 (mutation-epoch bump pin); MobileGL/MG_Remote/Client/ClientSession.cpp:1320+:1357 (RefusePipeInputsTouchWhileApplierOwnsIt); MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:55; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:62-67; MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526/535 | The role term becomes a constant in each process and the conjunction degenerates - but in two DIFFERENT directions, which is why these cannot be rewritten uniformly. In the client process `!RunsAsTheServerRole()` is always true (g_applyThreadKey==0), so the six CLIENT-arm sites reduce to plain SplitActive - mechanical. In the server process `OnApplyThread()` is true only on the apply thread, so the twelve SERVER refusals still have a live second term, and a non-apply thread in the server (the EGL/control thread, the main thread) that reaches PipeFill.cpp:975 or :1114 would take the CLIENT branch and emit client wire records with no ClientSession. That is a NEW reachability that inproc never had, because in inproc every thread except the apply thread genuinely was the client. | PipeFill.cpp:984-995's ClientWireRecordsEmitted() self-check would abort by name (Fatal{InitialBytesNotCarried}) on the | c6 | **claim-false** — Retire the two-spelling taxonomy. Reclassify by POLARITY, not by name, into three buckets: (a) server-arm gates that proceed when split AND on the apply thread - SlotAllocator.cpp:24-25, :30-31, :49-5 |
| A6-P3 | blocks-p6 | PATTERN: the four SlotCaps macros - `MGL_BACKEND_SLOT_CAP`, `MGL_BACKEND_SLOT_LOCAL`, `MGL_BACKEND_SLOT_PTR_CAP`, `MGL_BACKEND_SLOT_PTR_LOCAL`. Four DEFINITION lines that govern 14 non-test expansion sites. | MobileGL/MG_Remote/Client/SlotCaps.h:111, :116, :127, :134. Expansions: MG_Impl/GLImpl/Query/GL_Query.cpp (7), MG_Impl/GLImpl/Texture/GL_Texture.cpp (2), MG_Impl/GLImpl/Getter/GL_Getter.cpp (2), MG_Impl/GLImpl/Sync/GL_Sync.cpp (1), MG_Impl/GLImpl/Drawing/GL_Drawing.cpp (1), MG_Backend/Init.cpp (1) | The LOCAL forms are the interesting ones. `MGL_BACKEND_SLOT_LOCAL` answers `Transport == Monolith && slot != nullptr`, i.e. "I may call the backend directly". In the spawn SERVER process that is exactly what the server SHOULD do, and it would answer false if the server carries Transport != Monolith. One expansion is in MG_Backend/Init.cpp - bring-up code that runs in BOTH processes under spawn. c6 must decide whether LOCAL means "monolith" or "the backend is in this process", because under spawn those stop being the same statement. | none; a wrong LOCAL answer is a silently skipped call, not a Fatal | c6 | **overstated** — Restate as '4 definition lines governing 12 live expansion sites, all of them in MG_Impl/GLImpl, none in MG_Backend'. Delete the MG_Backend/Init.cpp sentence - it is the row's main argument for urgenc |
| A6-P4 | p6-must-handle | PATTERN: role-NEUTRAL split-active tests in the shared applier and codec. 9 code lines in MG_Pipe/PipeApply.cpp (11 minus 2 comments) plus MG_Remote/Wire/PipeWireCodec.cpp:1636 and MG_Backend/MGPipe/PipeInputs.cpp:292-294. | MobileGL/MG_Pipe/PipeApply.cpp:1022, :1043, :1311 (NoP4aConsumer), :1487 (MGPipeApplierReleaseObjectRecords), :2139 (the flush-range fault rule), :2245 (staged-texture drop), :2287 (MGPipeApplyMapPersistent's R-6 decline), :3462; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1636 (AdoptTier consult) | Nothing I can find. These are the most purely mechanical group in the census: `Transport != Monolith` -> `MGPipeSplitActive()` with no second question and no behaviour change. The one to watch is PipeApply.cpp:1311 (NoP4aConsumer), which reaches `ServerSession::Active()` - a singleton whose liveness in the spawn server process is a bring-up ordering question, not a role question. | Fatal{ProtocolCorruption} / Fatal{StageSnapshotTooNarrow} downstream | sm | **overstated** — State it as '8 role-neutral code lines in MG_Pipe/PipeApply.cpp (11 census lines minus 2 comments minus PipeApply.cpp:1353, which is A6-P2a's)'. The verdict p6-must-handle and the 'most purely mechani |
| A6-P5 | blocks-p6 | PATTERN: DIAL / bring-up. The sites that decide whether this process brings up a split at all. 3 code lines in MG_Backend/Init.cpp plus ClientSession::Start's signature and mode refusal. | MobileGL/MG_Backend/Init.cpp:266 (Init's THE SINGLE HOOK), :216 (ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint)), :236 (ShutdownSplitRoles' early-out); MobileGL/MG_Remote/Client/ClientSession.cpp:474 (Start's signature), :482 (the `mode != InProcess` named refusal); MobileGL/MG_Remote/Client/ClientSession.h:70 | This is where MGPipeShouldDial() has to be born, and the split of InitSplitRoles into a server half and a client half is the actual P6 work. Under spawn the CLIENT must run steps 3-4 without step 1 (no local ServerLoop, so g_applyThreadKey stays 0 - which is what makes every role probe in the client answer correctly for free), and the SERVER must run step 1-2 without step 3. Note Init.cpp:236's ShutdownSplitRoles calls ServerLoopInstance().Stop() UNCONDITIONALLY for M-6's reason; in a client that never created a ServerLoop that is a no-op, but it is a no-op by accident rather than by design. | Init.cpp:271-276 refuses to fall back to monolith when InitSplitRoles fails - the one gate that is already spawn-shaped | sm | |
| A6-P6 | blocks-p6 | The parse itself: `spawn` / `unix:` / `pipe:` are RECOGNISED, logged as "names a transport P6 implements and P5 does not", and then forced back to `MG_Config::Transport = Monolith`. | MobileGL/ConfigLoader.cpp:339-346 (MG_ConfigLoader::InitTransport); the four writes are :328, :332, :344, :350 | This is the first line c6 changes, and the trap is what replaces it. The refusal currently ends in `Transport = Monolith`, i.e. the refusal and the server's own resting state have the SAME spelling. The moment spawn is accepted, "I refused to dial" and "I am the server and do not dial" must stop being the same value or the 148 P1 sites cannot tell them apart. | the build-level gate CONTRACT-P5.md table 3 names (`nm --defined-only libMobileGL.so / grep -i MG_Remote` non-empty in b | c6 | **line-does-not-resolve** — Change the guard cite from Config.h:530-540 to Config.h:435-442. (The row's sibling cite in A6-P8, 'Config.h:424-440 argues it at length', DOES resolve - the 'WHY Transport IS NOT A FeaturesTable MEMB |
| A6-P7 | p6-must-handle | The sites that are ALREADY the natural carrier: named predicates whose entire body is one Transport test. Rewriting these is a one-line change each and it converts a large number of call sites for free. | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526-528 (PushIsArmed); MobileGL/MG_Remote/Client/GpuWritePending.cpp:125-127 (GpuWriteSetIsClientSide); MobileGL/MG_Backend/DirectGLES/Managers.h:740-742 (ProgramHandleArm), :1146-1152 (VertexInputReadsRecords); MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:272-274 (FramebufferRecordArmIsMandatory), :661 (ResolveBufferBindingSubsystemArm), :3386 (the sampler twin); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (MGPipeRoleSplitActive) | Nothing - but MGPipeRoleSplitActive (PipeInputs.cpp:292) is a NAME COLLISION waiting to happen: it means `Ipc.RoleSplitState && Transport != Monolith` (the P5f dual-block rehearsal), which is strictly narrower than the proposed MGPipeSplitActive(). Two predicates one word apart, one of which is gated on an env knob, is the kind of pair that drifts. | none | sm | |
| A6-P8 | blocks-p6 | The G1 constraint on the rewrite: in a build without MOBILEGL_BUILD_DISAGGREGATED, `inline constexpr TransportMode Transport = TransportMode::Monolith;` | MobileGL/Config.h:566 (the #else arm of the MOBILEGL_BUILD_DISAGGREGATED block); the comment stating why is at Config.h:563-565 | Not a spawn break - a REWRITE break. MGPipeSplitActive() / MGPipeServerArm() / MGPipeShouldDial() must each be `inline constexpr`-foldable to false in the pull build, or 226 sites become real branches and G1 goes red across the whole tree. The SlotCaps.h precedent (:139-147) shows the shape: the pull build gets a DIFFERENT macro expansion, not a runtime-false predicate. | G1 (the pull build's symbol set and codegen must not move) - a build-level gate that WILL go red for this reason | c6 | |
| A6-P9 | p6-must-handle | The FOURTH predicate that already exists and is not one of the three: MGPipeInstalledArm() -> {kNone, kMonolith, kClientWire}, "what the table actually does" as opposed to "what was ASKED for". | MobileGL/MG_Pipe/PipeRoute.h:234-239 (the enum, MGPipeInstalledArm, MGPipeNoteInstalledArm) with the contrast spelled at :235 | It is the honest answer to "am I the client arm" and it has no server value: kClientWire and kMonolith, no kServerApply. If c6 adds Role, this enum either grows a third value or becomes redundant with MGPipeShouldDial(). Leaving BOTH is how the tree ends up with two selectors that can drift apart - exactly what Managers.h:1142-1145 warns about for VertexInputReadsRecords. | MGPipeTablesAreInstalled() (PipeRoute.h:232) | c6 | |
| A6-P10 | later-stage | The backend reaching the ServerLoop singleton to find ITSELF: `if (Transport != Monolith) { if (BackendObject* server = ServerLoopInstance().Backend()) ... }` with the monolith arm reading MG_Backend::pActiveBackendObject. | MobileGL/MG_Backend/DirectGLES/Utils.cpp:50-62 (ActiveBackendFormatCaps); MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:685-699 (ApplyLineWidthState) | Not a break, an unnecessary indirection that becomes a constraint: it means the spawn server process MUST construct a ServerLoop singleton and register its backend there even though nothing in that process needs a loop to route through. If c6 lets the server process put its backend in pActiveBackendObject (there being no BackendObject_Remote to compete for the slot), these two sites stop needing the Transport test entirely - a behaviour-preserving simplification, but only if BOTH are changed together. | Utils.cpp:59-63's named refusal of the mirror fallback | cp | **overstated** — Widen the site list to Utils.cpp:55, VulkanRenderer.cpp:685, DirectVulkan.cpp:868, VertexInputStateFactory.cpp:256, :599, plus the ungated VertexInputStateFactory.cpp:464. Change the framing from 'an |
| A6-P11 | already-handled | Census correction: 12 of the 238 lines are COMMENTS, not code. 226 code lines remain. Counting occurrences of the bare symbol `MG_Config::Transport` (excluding TransportMode/TransportEndpoint) gives 228 non-test occurrences on 226 lines. | Comment-only lines: MobileGL/Config.h:564; MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2431; MobileGL/MG_Backend/DirectGLES/Managers.cpp:10389; MobileGL/MG_Backend/Init.cpp:263; MobileGL/MG_Impl/Pipe/ResourceTracker.h:55; MobileGL/MG_Pipe/PipeApply.cpp:20, :2134; MobileGL/MG_Pipe/PipeRoute.h:235; MobileGL/MG_Remote/Server/StagedShadow.h:34; MobileGL/MG_Remote/Server/StagedTextureStore.h:93; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:60; MobileGL/MG_State/GLState/BufferState/BufferObject.h:194 | nothing - but a rewrite package sized at 238 will overshoot by 12, and two of the twelve are inside the MG_State sample c6 is told to treat as representative | none | a6 | |
| A6-P12 | blocks-p6 | The MobileGLServer target that MOBILEGL_IPC_SERVER_PATH points at is the P0 spike STUB - an Android-only, OFF-by-default executable built from tools/spikes/server_stub/main.cpp that links no MobileGL code at all. | CMakeLists.txt:31 (the option), :926-949 (the target); MobileGL/MG_IntegrationTest/CMakeLists.txt:1931 (MGL_ITEST_SPLIT_SERVER_PATH = ${CMAKE_BINARY_DIR}/libMobileGLServer.so); tools/trace_replay/CMakeLists.txt:467 (IPC_SERVER_PATH = same); MobileGL/Config.h:452-457 (IpcTable::ServerPath, "P6 consumes it; P5 lands the parse") | a6 item 5's "link it once for real" experiment has no target to start from: the spawn server is not a variant of this executable, it is a NEW target that has to link MG_Backend + MG_Pipe + MG_Remote/Server and answer the question of whether MG_Impl and MG_State come along. Until that target exists, every claim in this audit about "what MG_Config::Transport is in the server process" is a design choice and not an observation. | none - a ctest ENVIRONMENT block naming a nonexistent .so is currently silent | c6 | **line-does-not-resolve** — Change the Config.h cite from :452-457 to :468-473 (struct IpcTable open at :468, comment :469-472, `String ServerPath;` at :473), and add MobileGL/ConfigLoader.cpp:358 as the sole parse site. Everyth |
| A6-P13 | p6-must-handle | Sites where the Transport test is spelled as a ?: SELECTOR across a #if boundary, splitting one expression over preprocessor arms rather than guarding a statement. Mechanically the hardest group to rewrite without changing shape. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:8396-8407 and :8413-8421 (useNative / resource selection), :8496-8513 (the twin block), :9361, :9518, :9782, :9831, :13574, :14590, :14831, :15153, :15600; MobileGL/MG_Backend/DirectGLES/Utils.cpp:2363-2366; MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3162-3167; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:958 | Not a semantic break, but a sed-and-commit rewrite will mangle these. I count ~20 of them in DirectGLES.cpp alone by form. They are the concrete reason the answer to "is the rewrite mechanical" is NO for MG_Backend even though every one of them is a pure ServerArm question. | G1 | sm | |
| A6-P14 | p6-must-handle | SUSPECTED ALREADY-WRONG-FOR-INPROC: the client-arm respecify and flush-range follow-ups guard against the apply thread with `!RunsAsTheServerRole()`, but only ONE of the two has a counter that can notice if the guard is wrong. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:975-1000 (respecify: has the ClientWireRecordsEmitted() before/after self-check and Fatal{InitialBytesNotCarried}) vs MobileGL/MG_Impl/Pipe/PipeFill.cpp:1114-1116 (flush-range: `if (Transport != Monolith && !RunsAsTheServerRole() && size != 0) MGPipeEmitResourceSubData(...)` with no check at all) | In the server process a NON-apply thread answers false to RunsAsTheServerRole() and would take the client arm here. The respecify site aborts by name. The flush site emits a resource_subdata into nothing and the picture is wrong until StageSnapshotTooNarrow fires on a later flush. I am flagging this as the asymmetry, not as a live bug: I did not find a path that reaches PipeFill from a non-apply thread in a server-role process today, because today there is no server-only process. | asymmetric: Fatal{InitialBytesNotCarried} at :987 for the respecify, only the downstream Fatal{StageSnapshotTooNarrow} f | sm | |
| A6-P15 | blocks-p6 | ANSWER TO "do the three predicates cover everything": yes for role, no for one axis. Three sites ask a question that is neither SplitActive nor ServerArm nor ShouldDial - they ask "does the backend live in THIS process". | MobileGL/MG_Remote/Client/SlotCaps.h:116 (MGL_BACKEND_SLOT_LOCAL) and :133-136 (MGL_BACKEND_SLOT_PTR_LOCAL), expanded at MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6837 and 13 other call sites including MobileGL/MG_Backend/Init.cpp | Under spawn there are two processes, and in the SERVER process the backend IS local while a transport IS active. The macro's two terms stop being the same question. This is the fourth pattern the task asked me to name if the three did not cover it: LOCALITY, not role. It is small - 2 macro definitions, 14 expansions - but it is real, and folding it into ShouldDial ("if I do not dial, the backend is local") is a coincidence in the current topology rather than a definition. | none - a wrong answer is a silently skipped backend call | c6 | **overstated** — Restate as '2 macro definitions, 2 live expansion sites, both at MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6552 and :6837'. Keep the verdict - the locality/role conflation is real and c6 must rul |
**复审另行指出的遗漏**:
- MobileGL/ConfigLoader.cpp:408 - `if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;`. This is the SEVENTH Config-dir census line and NO row in the item names it (A6-P6 accounts only for :326, :328, :332, :344, :350 and Config.h:564). It gates the one-line resolved-IPC-configuration log at :411-417 ('Config: IPC ring=%uMiB stage=%uMiB ... role-split-state=%d affinity=...'). It matters directly to this item's own open question #2, which asks for 'a first-boot log line in the server that prints the resolved Role so a wrong answer is visible in the first 20 lines of logcat': the site that would carry that line already exists and is gated on the exact bit under dispute, so a spawn server left at Transport==Monolith prints NOTHING and silences the very diagnostic the audit asks for.
- MobileGL/MG_Backend/DirectGLES/Managers.cpp:1161-1166 - `MG_Remote::Server::StagedShadowStore& ServerStaged() { static ... store = *new StagedShadowStore(MG_Config::Transport != MG_Config::TransportMode::Monolith); return store; }` (the Transport read is census line :1164). This is the exact family twin of StagedTextureStore.h:475, which A6-P1b names as one of its three bring-up-once sites - and MobileGL/MG_Remote/Server/StagedTextureStore.h:466-467 literally says it follows 'ServerStaged() ruling verbatim'. Its own comment at :1155-1157 gives the same once-and-for-all argument ('an answer that changed mid-run would strand every resource already staged'). A6-P1b names the derivative and misses the original. It is also the wider of the two: ServerStaged() governs MGL_SERVER_STAGED_ADOPT/DROP/DROP_ALL/REQUIRE_COVERAGE at Managers.cpp:1191, :1201-1205, :3052, :3398, :3533.
- MobileGL/MG_Impl/Pipe/PipeFill.cpp:2868-2869 and :2913-2914 - the g_fillPlan MEMO KEY: `&& g_fillPlan.CapsGeneration == (MG_Config::Transport != Monolith ? MG_Remote::Client::CapsMirrorInstance().Generation() : 0)` and the matching store. No row names either line. This is the ONLY shape in the whole 226-line census where the Transport answer feeds a CACHE KEY rather than a branch, which makes it the one site where a predicate rename can change WHEN the answer is computed and therefore change cache-invalidation semantics rather than just control flow. It is also a direct counterexample to A6-P7's organising quote from Managers.h:737-739 ('NOT LATCHED... Transport is configuration read once at bring-up'): here the Transport answer selects between a latched generation counter and a constant 0. And CapsMirrorInstance() is client-side structure, so if c6 gives the server process Transport != Monolith this code consults a mirror that has no meaning there.
- MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6552 - `MGL_BACKEND_SLOT_LOCAL(GetTextureImage)` (in the `pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan` arm). A6-P15 names only its twin at :6837 and then pads the list with 13 sites that belong to a different macro family. :6552 is the OTHER of the two real locality sites and it is the more interesting one, because it conjoins the locality question with a backend-type question against pActiveBackendObject - which A6-P10 establishes holds BackendObject_Remote on the client (Init.cpp:225-228), so in a spawn client this arm asks 'is the backend local' of an object that by construction is not.
- MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp:256-260, :599-603 and :464, plus MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:868-873 - four more sites in A6-P10's family (backend reaching ServerLoopInstance().Backend() for its own dynamic parameters). P10 asserts there are exactly two and that they must 'be changed together'. VertexInputStateFactory.cpp:464 is the sharpest of the four: it reads `ServerLoopInstance().Backend()` with NO Transport test at all (inside `#if MOBILEGL_BUILD_DISAGGREGATED` opened at :438) and silently treats a null answer as 'fp64 unsupported' at :465-467.
- MobileGL/MG_Remote/Client/WireTables.cpp:87 - `Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }`. Not a census line, but the single fact that collapses A6-P2a's two-spelling taxonomy, and the item never opens it despite building a blocks-p6 row on the premise that the two names differ in meaning. c6 needs this on the record: there is exactly ONE role predicate in the tree under two names, so the Role axis has one implementation to redirect, not two.
- MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp:66-67 - the comment `// This is the same pointer expression MGL_BACKEND_SLOT_PTR_LOCAL had in a pull build.` followed at :67 by the RAW `if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync)`. A6-P3 counts this as an expansion; it is the opposite - a site where the macro was deliberately declined and the locality question left spelled with no Transport term at all. It is a third instance of A6-P15's locality question and the only one that would be invisible to a rewrite that greps for the macro names.
---
## a6.7 abort 普查
**小结**:At b3b9c2ee there are 92 executable `std::abort()` statements under MobileGL/MG_Remote/ (94 grep hits minus two inside comments: EmitTables.h:25, WireLog.h:49); one of the 92, ClientSession.cpp:44's MGP5_C0_STUB, is defined at :39 and #undef'd at :1549 with zero expansions, so 91 can fire. Roughly 48 of them are reachable from bytes the PEER wrote, split ~27 server-side (records, SEG_STAGE, ctrl frames, the shared control page) and ~21 client-side (SEG_EVENT, reply slots, Welcome, the watermarks the server advances); the other ~44 are self-checks on our own invariants. Two shared funnels multiply that: Wire::WireProtocolFatal/At (PipeWireCodec.cpp:377/382) has 54 call sites, and the generated MGPipeWireProtocolFatal (PipeWire.inc:1177) has 84 MGP_WIRE_CHECK_BOUNDS arms plus the `<unknown opcode>` default at :1472 - every one of those 139 is peer-reachable by construction. ANSWER TO THE DECISION QUESTION: funnelling into one `Session::Fail(FatalCode, detail)` that is still `[[noreturn]]` and still aborts is a RENAME for all 92 sites plus the ~90 MG_Pipe ones - no site loses anything, because nothing after an abort is reachable today. The expense is entirely in the LATER latch flip, and it is 13 sites where a returning Fail is undefined behaviour with no honest value to return (8 of them reference-returning: EmitTables.cpp:216, WireTables.cpp:120, ServerSession.cpp:272/:698/:713/:724/:731/:739; 5 last-statement non-void: PipeWireCodec.cpp:927/:2312, ServerSession.cpp:262, ServerLoop.cpp:1056, StagedTextureStore.h:344, PersistentMapTracker.cpp:1059) plus ~9 where falling through IS the defect the Fatal exists to prevent (ServerSession.cpp:158's guessed CallMask is the named "marquee defect"; EmitTables.cpp:843's silent zero bpp; PipeWire.inc:1472's unknown-opcode dispatch). The remaining ~70 need one inserted `return`. THE ONE THING c6 MOST NEEDS: `ServerSession::ConsumerDoorbell()`/`ProducerDoorbell()` abort `Fatal{UnmigratedVerb}` for every TransportRole that is not InProcess (ServerSession.cpp:719-724, :735-739), and ServerLoop::ApplyThreadMain calls ConsumerDoorbell() unconditionally at ServerLoop.cpp:373 - so the first spawn session aborts on the apply thread before it pops a single record. That is a blocks-p6 row, not a hardening row. Separately, three guest-reachable unbounded allocations are real and verified: PipeApply.cpp:1566-1568 (CreateRenderState resizes RenderStateCsos to a peer-supplied Cso.Slot+1 with NO slot limit, unlike every sibling family which goes through RecordAt's bound), PipeApplier.cpp:439-440 (readback scratch from peer w*h*bpp), and ProgramArtifactsCodec.cpp:219-220 (count bounded by remaining BYTES then multiplied by sizeof(element)) - and there is not one try/catch anywhere under MG_Remote/, so each one ends in an uncaught std::bad_alloc and an unnamed std::terminate.
| A7-1 | blocks-p6 | ServerSession::ConsumerDoorbell() / ProducerDoorbell() abort Fatal{UnmigratedVerb} for every TransportRole except InProcess; ApplyThreadMain calls ConsumerDoorbell() unconditionally | MobileGL/MG_Remote/Server/ServerSession.cpp:709-724 (ServerSession::ConsumerDoorbell), :727-739 (ServerSession::ProducerDoorbell), reached from MobileGL/MG_Remote/Server/ServerLoop.cpp:373 (ServerLoop::ApplyThreadMain) | The spawn transport's role is not InProcess, so mgl-srv-apply aborts at ServerLoop.cpp:373 before it pops its first record. The abort text says 'that is P6's SocketTransport' - it is the phase's own tripwire, correctly armed, and it fires first. | The Fatal itself is the guard; it goes red for exactly the reason it exists. | so | |
| A7-2 | blocks-p6 | MGPipeApplyCreateRenderState resizes g_applier.RenderStateCsos to a peer-supplied Cso.Slot+1 with NO slot limit; every sibling create-family goes through RecordAt(records, slot, slotLimit) | MobileGL/MG_Pipe/PipeApply.cpp:1563-1569 (MGPipeApplyCreateRenderState) vs :471-476 (RecordAt) and its bounded callers at :1884, :2397, :2650, :2752, :2811, :1210/:1216; decode arm MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1713 | A record with Cso.Slot = 0xFFFFFFFE asks for ~4.29e9 entries of MGPipeRenderStateCsoRecord (8 + kMGPipePipelineChunkBytes = 396, so ~1.7 TB). std::length_error / std::bad_alloc, uncaught, std::terminate - no Fatal line, no family tag. | none - MOBILEGL_ASSERT at :1564 checks only slot != 0 and is inert at INFO | dl | |
| A7-3 | already-handled | Wire::WireProtocolFatal / WireProtocolFatalAt: the [[noreturn]] funnel this layer already has, 54 call sites | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:377-380, :382-386 (definitions); declarations MobileGL/MG_Remote/Wire/PipeWireCodec.h:110-111 | nothing - this is the shape dl wants, one level down. It is the proof that the funnel is a rename: 54 sites already route through two functions. | n/a | dl | **claim-false** — State 93 call sites tree-wide, 54 of them in PipeWireCodec.cpp. Keep the verdict already-handled and the rename argument - it gets STRONGER, not weaker - but c6 must be told the rename touches six fil |
| A7-4 | p6-must-handle | Generated MGPipeWireProtocolFatal: 84 MGP_WIRE_CHECK_BOUNDS arms plus the `<unknown opcode>` default, all peer-reachable, all [[noreturn]]-relying | MobileGL/MG_Pipe/generated/PipeWire.inc:1177-1181 (definition), :1183-1188 (MGP_WIRE_CHECK_BOUNDS), :1472 (the default arm, inside a Bool function that continues to gMGPipeWireRecordApply dispatch at :1474-1477) | Nothing breaks; it is exactly right. But it is GENERATED, so the funnel rename must be a generator edit, and if the latch ever flips, :1472 needs `return false` or an unknown opcode is handed to the real decoder. | the check itself | dl | **claim-false** — 83 arms plus the default = 84 peer-reachable sites behind MGPipeWireProtocolFatal. Total peer-reachable death sites behind the two named funnels is 177, not 139. |
| A7-5 | later-stage | Eight reference-returning Fatal sites: a returning Fail is a null dereference or a missing return, and there is no value to invent | MobileGL/MG_Remote/Client/EmitTables.cpp:208-217 (RequireSession -> ClientSession&, `return *session;` at :217); MobileGL/MG_Remote/Client/WireTables.cpp:106-121 (RequireSession); MobileGL/MG_Remote/Server/ServerSession.cpp:267-273 (ReverseCallbackOwner -> ServerSession&), :695-701 (Control -> RingControl&, `return *control;` at :701), :713 and :724 (ConsumerDoorbell -> Doorbell&), :731 and :739 (ProducerDoorbell -> Doorbell&) | nothing today. These are the sites that make the LATER latch flip a redesign: a Doorbell& or a RingControl& has no null, so each caller must be restructured to ask a Bool question first. | none needed while Fail is [[noreturn]] | Ph | **line-does-not-resolve** — EmitTables.cpp:208-218, return at :218. ServerSession.cpp:693-701, abort at :698, return at :700. Verdict later-stage / phase Ph unchanged - the row's argument (a Doorbell& or RingControl& has no null |
| A7-6 | later-stage | Five last-statement non-void Fatal sites: a returning Fail is UB (falls off the end of a value-returning function) | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:927 (PipeWireEncoder::StageAllocate -> Uint8*), :2312 (PipeWireDecoder::ApplyChecked -> Bool, the switch default); MobileGL/MG_Remote/Server/ServerSession.cpp:262 (ReserveEventOrBlock -> void*); MobileGL/MG_Remote/Server/ServerLoop.cpp:1056 (ServerLoop::ApplySurfaceControlFrame -> MobileGLResult); MobileGL/MG_Remote/Server/StagedTextureStore.h:344 (RequireLevelBytes -> const Uint8*); MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:1059 (AdoptTierIsEmulate -> Bool) | nothing today. Each has a plausible-but-lying return (nullptr, false, an error code) that a caller would then have to check - StagedTextureStore.h:344's callers dereference the result, so nullptr is the crash one line later. | none | Ph | **claim-false** — Say six sites. Move PipeWireCodec.cpp:2312 into A7-3's funnel bucket where it belongs. Re-file StagedTextureStore.h:344 as its own finding: the function ALREADY has a nullptr return path its two produ |
| A7-7 | later-stage | ServerSession::CallMask() falls through FatalUnsetCallMask to `m_capBits / MGCapsConsumerBits(...)` - the fall-through IS the defect the Fatal names | MobileGL/MG_Remote/Server/ServerSession.cpp:147-159 (FatalUnsetCallMask, [[noreturn]]), called from :415-420 (ServerSession::CallMask, returns Uint64) | nothing today; but it is the clearest example of a site where a returning Fail silently re-creates a named historical defect. Any latch flip must give this one a refusal path, not a return value. | none after the Fatal | Ph | **line-does-not-resolve** — ServerSession.cpp:412-417, call at :414, fall-through at :416, Bool twin at :410. Verdict later-stage / phase Ph unchanged. |
| A7-8 | p6-must-handle | Ring watermark advance: Fatal{ProtocolCorruption,"watermark"} fires on a value read out of the SHARED control page | MobileGL/MG_Remote/Transport/Ring.cpp:364-377 (AdvanceMonotonic), call sites :390 submittedSeq, :394 appliedSeq, :403 retiredSeq, :407 completedFrameSerial, :411 presentAckSerial; page layout MobileGL/MG_Remote/Transport/Ring.h:135-139 | Both processes map the control page read-WRITE (ShmSegment.cpp:283 and :346 both call Map(false); the readOnly parameter at ShmSegment.h:64 has no true caller). A client that stores a huge appliedSeq into the shared page makes the SERVER abort here - a peer-written byte reaching a Fatal that was written as a self-check. | the Fatal itself, but it is aimed at our own bug, not at the peer | cp | **claim-false** — Both roles write cursors into the SAME page: RingControl is one 4096-byte object (static_assert at Ring.h:150), so the row's natural_carrier - 'map the control page read-only on the side that does not |
| A7-9 | p6-must-handle | Four SEG_EVENT decode Fatals: the client aborts on a malformed reverse-channel record | MobileGL/MG_Remote/Client/ClientSession.cpp:268-370 (DrainEventRing): :277 corrupt record stream, :290 kEventBufferWriteback head->Size vs tail, :316 kEventGpuWritten RangeCount vs tail, :362 kEventGlError MessageBytes / missing NUL | Correct as written - these are real peer gates. What changes is the consequence: a server that dies mid-record now kills the app's GL thread with an abort instead of latching the context lost. | the four checks themselves, all named | dl | |
| A7-10 | already-handled | Nine decoder honesty Fatals on peer blobrefs and spans - the densest peer-reachable cluster in the tree | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:455-514 (CheckBlobIsHonest, aborts at :466 :474 :504 :512), :516-523 (RequireDeclaredBlob, :521), :543-565 (CheckHostSpanIsHonest+segments, :562), :1498-1521 (ApplyChecked's tail cross-check, :1520), :1799 (CreateShaderState.Spirv declared), :2074 (ResourceSubData union box) | nothing breaks - this is the part of the census that is already right. It is the bulk evidence that the funnel is a rename: every one is `if (bad) { MGLOG_F(...); std::abort(); }` inside a void or continuing function. | the checks themselves | dl | |
| A7-11 | p6-must-handle | PipeApplier peer-record Fatals: Present.FrameSerial, ApplierReset.ContextSerial, ObjectDeath.Handle/.Kind, the two sampler-window checks | MobileGL/MG_Remote/Server/PipeApplier.cpp:325-334 (OnPresent, FrameSerial 0 under run-ahead), :1144-1157 (OnApplierReset, ContextSerial), :1168-1183 (OnObjectDeath, null handle and Kind >= KindCount), :550-572 (CheckUnitWindows, both arms) | All six are exactly the shape a merely-unusual peer produces - a restarted client whose reset count disagrees, a present serial of 0 from a peer built before P5e. Each is a remote kill of the server. | the checks themselves, all named ProtocolCorruption | dl | |
| A7-12 | already-handled | ServerLoop::DrainRing aborts on a corrupt SEG_CMD record header - the canonical peer gate | MobileGL/MG_Remote/Server/ServerLoop.cpp:617-626 (ServerLoop::DrainRing), detection at MobileGL/MG_Remote/Transport/Ring.cpp:246-262 (RingConsumer::Pop sets *outCorrupt and returns false, no abort) | nothing breaks. Note the good split already present: the TRANSPORT returns a flag, the SESSION decides to die. That is the shape the funnel wants everywhere. | Ring.cpp:251-253's bounds test | dl | |
| A7-13 | p6-must-handle | Wire SurfaceOp validation aborts on the server: an ANativeWindow*, a CAMetalLayer*, an unknown wire kind, an out-of-range windowBackend tag, an undispatchable op | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:197-218 (ServerApplyWireSurfaceOp); MobileGL/MG_Remote/Server/ServerLoop.cpp:864-871 (WindowBackendFromFrameValue), :1053-1057 (ApplySurfaceControlFrame's unknown-kind tail) | Under spawn the same frame arrives from the peer's flatbuffer, so ServerLoop.cpp:860-863's premise ('an out-of-range tag is a memory-corruption shape, not bad input') is false and the comment must be corrected in the same change. WindowBackendFromFrameValue also falls through to a cast of an out-of-range enum if the Fatal ever returns. | the three checks, all named | sm | |
| A7-14 | p6-must-handle | Handshake aborts on both sides: FatalAbiMismatch on a peer's Hello / Welcome | MobileGL/MG_Remote/Server/ServerSession.cpp:92-108 (FatalAbiMismatch), called at :471-477 (protocol version) and :479-482 (struct fingerprint); MobileGL/MG_Remote/Client/ClientSession.cpp:83-92, called at :566-568 | A mismatched or hostile peer kills the other by sending a Hello/Welcome. Note the refusal the review points at is real and eight lines away: ServerSession.cpp:456-466 already answers MOBILEGL_ERR_PROTOCOL_MISMATCH for a malformed frame without aborting. Also: both FatalAbiMismatch calls fall through into Accept()/Start() continuing, so a returning Fail would mount shared memory for an ABI-mismatched peer. | the fingerprint comparison itself | so | |
| A7-15 | p6-must-handle | Reply-path aborts on the client: the server's status byte and reply size can kill the app | MobileGL/MG_Remote/Client/ClientSession.cpp:1104-1110 (ReplyMissing), :1121-1127 (ReplyTooLarge); MobileGL/MG_Remote/Client/EmitTables.cpp:883-909 (RequireReadbackReplyComplete, four arms at :887 :894 :898 :907); MobileGL/MG_Remote/Client/WireTables.cpp:441-446 and :487-493 and :500-505 | A peer that posts status 2, a short OK payload, or a slot whose header stamps the wrong seq turns a glReadPixels into an abort. The client-side read already refuses an over-cap header.Size by returning false (ReplySlot.h:295-302), which is the non-abort shape to copy. | ReplySlot.h:295-302 bounds header.Size before the memcpy at :314 | dl | |
| A7-16 | p6-must-handle | Four timeout/credit aborts on the client: a peer that simply stops is indistinguishable from a wedge, and the answer is abort | MobileGL/MG_Remote/Client/ClientSession.cpp:1071-1078 (BarrierTimeout), :1206-1214 (forced run-ahead wait), :1271-1279 (PresentCreditTimeout), :911-916 (RetirementWaitFailed, SEG_CMD); and the encoder's twin at MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:921-928 (RetirementWaitFailed, SEG_STAGE) | A server process that is merely slow, swapped out or SIGSTOPped kills the client after kBarrierTimeoutMs. Under spawn 'the server is gone' becomes an ordinary event (SIGKILL, OOM-kill, a crash) and it must latch, not abort - this is precisely the S2 gate the review re-aims at P6.5. | the bounded wait itself | P6.5 | **line-does-not-resolve** — protocol.fbs:254. Trivial, but the item's own doc_drift section corrects a fourteen-line drift in P6-CONTRACT-DRAFT.md for this same enum, so the census should not introduce a one-line one of its own. |
| A7-17 | later-stage | SEG_EVENT overflow policy: the server parks 30 s on a client's bell and then aborts | MobileGL/MG_Remote/Server/ServerSession.cpp:212-263 (ReserveEventOrBlock): :218 non-run-ahead immediate Fatal, :230 record too big for the ring, :244-251 the 30 s wait Fatal, :259-262 the emptied-ring Fatal; constant kEventBacklogWaitMs at :210 | A guest that simply never calls Drained() parks the host's renderer for 30 s and then kills it. The review names this; it is real and the 30 s constant is the whole of the policy. | kEventBacklogWaitMs = 30000 (ServerSession.cpp:210) - bounded, but bounded at a number that is already a hosting outage | Ph | **line-does-not-resolve** — Use :216. And add the cross-reference the row misses: ReserveEventOrBlock takes `Transport::Doorbell& bell = session.ConsumerDoorbell();` at ServerSession.cpp:234, so the whole SEG_EVENT backpressure |
| A7-18 | p6-must-handle | Readback scratch is resized from a peer-supplied w*h*bpp with no absolute cap, and the resize happens BEFORE any size gate | MobileGL/MG_Remote/Server/PipeApplier.cpp:416-441 (ServerVerbSink::OnReadPixels): `tight` computed at :425-430 from info.Box.W/H and the format pair, `m_readbackScratch.resize(tight)` at :439-440 | A record with Box.W = Box.H = 65535 and RGBA8 asks for a 17 GB resize on the apply thread. bad_alloc, uncaught, terminate. The server also then calls PostReply with `tight`, which ReplySlot.h:235-246 would refuse - but only after the allocation. | none on the server side; the only bound is the client's own pre-emit check, which a peer does not have to run | dl | |
| A7-19 | p6-must-handle | Program archive decode: a count bounded by remaining BYTES is then multiplied by sizeof(element) | MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp:166-175 (TakeCount), :216-223 (ArchiveVector arm: `value.resize(count)`); reached from MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1814-1818 (DecodeProgramArchive on peer-staged bytes) | With SEG_STAGE at its default 32 MiB, a forged archive can declare a count of ~32e6 for a vector whose element is tens of bytes, so the resize is one to two orders of magnitude larger than the archive. The outer-then-inner nesting lets a second such resize live at the same time. The top-level DecodeProgramArchive stage count IS bounded (:335-338, kProgramArchiveMaxStages); the recursive arm is not. | TakeCount's byte bound (real, but pre-multiplication) | dl | |
| A7-20 | p6-must-handle | No try/catch anywhere under MG_Remote/, and exceptions are not disabled for MobileGL - every peer-driven allocation ends in an unnamed std::terminate | grep for try/catch over MobileGL/MG_Remote/**.{cpp,h} returns zero handlers (only struct names match); -fno-exceptions appears only under 3rdparty/apitrace. Allocation sites: MobileGL/MG_Pipe/PipeApply.cpp:1567, MobileGL/MG_Remote/Server/PipeApplier.cpp:440, MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp:220, MobileGL/MG_Remote/Server/StagedShadow.h:72, MobileGL/MG_Remote/Server/StagedTextureStore.h:441, MobileGL/MG_Remote/Server/ServerSession.cpp:68, MobileGL/MG_Remote/Client/ClientSession.cpp:67 | Every resource-exhaustion row above lands as std::terminate with no Fatal line, no family tag and nothing on stderr for a death control to match - the exact failure WireLog.h:35-52 was written to end. | none | dl | **claim-false** — Four verified unbounded peer-driven allocations, not three. Add StagedTextureStore.h:441 as its own blocks-p6 row with the StagedTextureRunImageOffset analysis, and note that StagedTextureStore::Adopt |
| A7-21 | later-stage | MOBILEGL_ASSERT compiles out at INFO (the production default), so every assert-only diagnostic on a peer-supplied handle is silent in shipping builds | MobileGL/Defines.h:104-115 (MOBILEGL_ASSERT is live only when MOBILEGL_LOG_ACTIVE_LEVEL <= DEBUG, and expands to TRAP, not abort); peer-reachable users MobileGL/MG_Backend/DirectGLES/SlotTables.h:347-351 and :370-376, MobileGL/MG_Pipe/PipeApply.cpp:1564-1565 | A forged handle takes the silent refusal path. IMPORTANT NEGATIVE RESULT: in SlotTables.h the REFUSAL is a real statement (`return m_nullTwin;` at :351 and :376) outside the assert, and PipeApply.cpp:660-700's resolvers count every refusal into RefusedResourceCalls / RefusedObjectCalls - so the BOUND holds in release and the drop is observable. What is missing is the NAME, not the bound. | the real `return` statements beside the asserts, plus the Refused* counters (PipeApply.cpp:663-700) | Ph | |
| A7-22 | p6-must-handle | Staged-shadow coverage Fatals: the server aborts because a record the peer should have sent did not arrive | MobileGL/MG_Remote/Server/StagedShadow.h:99-116 (RequireCoverage), MobileGL/MG_Remote/Server/StagedTextureStore.h:326-345 (RequireLevelBytes) | This is the purest 'merely unusual peer message' case in the census: a peer that draws from a buffer it never filled kills the host renderer with Fatal{StageSnapshotTooNarrow}. RequireLevelBytes returns const Uint8* and its callers dereference it, so it is also an A7-6 site. | the coverage set itself (StagedShadow.h CoverageHas) | dl | |
| A7-23 | p6-must-handle | Two abort sites carry NO Fatal{...} family tag, so a verbatim funnel leaves them invisible to the family grep | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:1040-1060 (AdoptTierIsEmulate: two MGLOG_F lines, neither with a Fatal{} marker, then abort at :1059); MobileGL/MG_Remote/Transport/WireLog.cpp:34-51 (WireLogFatal formats whatever its caller passed - four ReplySlot.h callers do carry Fatal{}, Ring.cpp:370 does too) | nothing breaks, but the review's promise that the funnel keeps the log 'verbatim' means these two arrive at Session::Fail with no FatalCode to map to. AdoptTierIsEmulate is also a value-returning function whose abort is its last statement (A7-6). | none - a CI grep for Fatal{ would not see either | dl | |
| A7-24 | already-handled | Control-frame receive resizes a vector from a peer-declared frame length - bounded, and the bound is a named refusal already | MobileGL/MG_Remote/Server/ServerSession.cpp:58-70 (ReceiveEnvelope, `out.resize(size)` at :68) and MobileGL/MG_Remote/Client/ClientSession.cpp:59-69 (:67); the bound is MobileGL/MG_Remote/Transport/Framing.h:48 (kMaxFramePayloadSize = 64 MiB) enforced at :173-179 with a WireLogError and MOBILEGL_ERR_PROTOCOL_MISMATCH, plus the send-side check at :58-63 | Only that 64 MiB per frame is still a large peer-driven allocation on a control plane whose real messages are kilobytes. Not a blocker. Recording it so c6 does not re-derive it: the framing layer is the ONE place in the tree that already does refusal-not-abort on a peer-supplied size, and it is the model for the rest. | Framing.h:173-179, a real named refusal that can go red | dl | |
| A7-25 | no-action | MultiDraw arrays resized from a peer-supplied NumDraws - bounded transitively by the record-size cross-check | MobileGL/MG_Remote/Server/PipeApplier.cpp:676-710 (ServerVerbSink::OnDrawVbo's multi-draw arm), resizes at :687 :690 :699 :700 from info.NumDraws; the bound is MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1511-1521 (size == layout.TotalBytes) composed with MobileGL/MG_Remote/Transport/Ring.cpp:250-262 (size <= available && offset+size <= capacity) | nothing. NumDraws is capped at (SEG_CMD capacity)/sizeof(MGPDrawRange), ~350k at the 8 MiB default, so the four resizes are at most a few MB. A legitimate and useful negative result: the tail cross-check at :1520 is load-bearing as an ALLOCATION bound, not just a decode bound, and anything that weakens it re-arms this row. | PipeWireCodec.cpp:1511-1521, and it can go red for exactly this reason | dl | |
| A7-26 | p6-must-handle | Encoder-side Fatals in PipeWireCodec are CLIENT self-checks, not peer-reachable - but two of them wait on a watermark the peer advances | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:834-928 (PipeWireEncoder::StageAllocate): :865 blob larger than SEG_STAGE at any occupancy (self), :912 and :927 the retirement wait (peer-paced); :973-1145 (EncodeRecord): :1030 record over MaxRecordBytes (self), :1140 a declared CreateShaderState.Spirv run (self) | :912 and :927 are the sites where a server that stops retiring kills the client after 5 s; the other two stay honest self-checks that never involve a peer byte. Worth keeping the distinction in the funnel's detail string so an operator can tell which half was wrong. | the 5000 ms bell wait at :917 | dl | |
| A7-27 | no-action | Dead abort site: MGP5_C0_STUB is defined and #undef'd with zero expansions | MobileGL/MG_Remote/Client/ClientSession.cpp:39-44 (definition, abort at :44), :1549 (#undef); no expansion anywhere in the tree | nothing. Recording it so the census count is honest: 92 statements, 91 that can execute. | n/a | dl | |
| A7-28 | p6-must-handle | PipeRoute's six reply-mailbox Fatals and PipeApply's three role/emulation Fatals are applier-reachable and must be in the funnel's scope, but are NOT peer-reachable | MobileGL/MG_Pipe/PipeRoute.cpp:85-97 (MGPipePostReply), :105-120 (TakeReply), :137-143 and :148-153 (TakeReplyBool/Pointer), :258-267 (Mono_SetResidualValueState); MobileGL/MG_Pipe/PipeApply.cpp:1353-1360 (MGPipeApplierReset off the apply thread), :3460-3467 (MGPipeUnmigratedEmulation), :63-70 (MGP_TRIP_WIRE_REPORT); MobileGL/MG_Pipe/generated/PipeFilled.inc:412-416 (MGPipeInputPoisonFatal) | Nothing peer-driven. Flagging two things c6 needs: (a) MGP_TRIP_WIRE_REPORT at PipeApply.cpp:63-70 only aborts under MOBILEGL_PIPE_POISON/VERIFY and is MGLOG_E otherwise, so it is a third policy alongside abort and assert and the funnel must not silently absorb it; (b) MGPipeInputPoisonFatal is a second [[noreturn]] funnel in generated code with its own family word. | n/a | c6 | |
| A7-29 | p6-must-handle | Both processes will map every segment read-write; the readOnly parameter exists and has no true caller | MobileGL/MG_Remote/Transport/ShmSegment.h:64 (Map(bool readOnly)), :71 (MappedReadOnly); the only two callers are MobileGL/MG_Remote/Transport/ShmSegment.cpp:283 and :346, both Map(false); POSIX prot at ShmSegmentPosix.cpp:159, Win32 at ShmSegmentWin32.cpp:129 | Every shared cursor, watermark and parked flag (Ring.h:102-147) is writable by both sides, which is what makes A7-8 a peer-reachable abort and what turns SEG_REPLY and SEG_EVENT into surfaces the peer can scribble on. The plumbing to do better is already there and unused. | none today | cp | **claim-false** — Restate as: the RO path is built AND tested (FdPassingTest.cpp:116, :188); only the two production call sites pass false, deliberately and with a comment saying why. Then add the contradiction the row |
| A7-30 | blocks-p6 | The schema's FatalCode has 7 values; the tree's Fatal{} vocabulary is 30 distinct family words plus 2 untagged sites | MobileGL/MG_Remote/Protocol/protocol.fbs:248-256 (FatalCode: None, ProtocolCorruption, RingOverrun, SegmentMismatch, DeviceLost, ServerCrashed, AbiMismatch) and :258-261 (table Fatal); the only reference to the enum in C++ is MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp:125 | The funnel's signature Session::Fail(FatalCode, detail) cannot carry today's 30 families in its first argument. Counts by family across the 92 sites: ProtocolCorruption 37, RoleViolation 8, RingOverrun 4, UnmigratedVerb 4, EventRingOverflow 4, NoClientSession 3, ReplyError 3, AbiMismatch 2, RetirementWaitFailed 2, BarrierTimeout 2, StageSnapshotTooNarrow 2, and 20 families with one site each, plus 2 untagged. Only 3 of the 30 map onto an existing FatalCode; SegmentMismatch has zero producers and PipeWireCodec.cpp:495-509 argues explicitly AGAINST minting a family for the case it names. | none - nothing today can notice the mismatch | c6 | **overstated** — Give c6 the family LIST (31 words, reproducible by `grep -rn 'Fatal{[A-Za-z]' MobileGL/MG_Remote/ / grep -v '^[^:]*:[0-9]*: *//' / grep -o 'Fatal{[A-Za-z]*' / sort -u`) and drop the invented per-famil |
**复审另行指出的遗漏**:
- MobileGL/MG_Remote/Server/StagedTextureStore.h:441 - a FOURTH unbounded peer-driven allocation the item lists as an allocation site (in A7-20) but never flags. `shadow.Bytes.resize(needed, 0)` in CopyRunInto, needed = StagedTextureRunImageOffset(upload, regions) + byteSize. The offset function at MobileGL/MG_Remote/Server/StagedTextureStore.h:178-193 guards only `upload.Blob.Size != boxBytes` (:190); with box.D == box.H == 1 that reduces to Blob.Size == box.W * texelBytes and leaves box.Z, box.Y, regions[0].SrcSliceStride and SrcRowStride entirely free. Returned offset at :192-193. Reached from MobileGL/MG_Pipe/PipeApply.cpp:1037-1039 and MobileGL/MG_Backend/DirectGLES/Managers.cpp:2738-2740. Deserves its own blocks-p6 row beside A7-2 / A7-18 / A7-19.
- MobileGL/MG_Remote/Server/StagedShadow.h:72 - a FIFTH candidate. `if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);` with `needed = std::max<SizeT>(width, offset + size)` at :71. Reached through the macro at MobileGL/MG_Backend/DirectGLES/Managers.cpp:1200-1201. MobileGL/MG_Pipe/PipeApply.cpp:707-711 (BufferRangeFault) bounds offset+size against `width`, but nothing I found bounds `width` itself - it is the resource's peer-declared storage. INFERRED, not verified: I did not trace create_resource's width validation to the end, so c6 should resolve this one before acting on it.
- MobileGL/MG_Remote/Server/QueryServer.inc:5, :12, :16, :21, :34, :50, :80, :91, :111, :118, :130, :144 - twelve server-side Wire::WireProtocolFatal arms on peer-supplied query records (handle, Kind, lifecycle state, `request.Wait > 1`, `request.Reserved`). Peer-reachable by construction, in the funnel's rename scope, named by no row. A7-3 swallows them in a count that is itself wrong.
- MobileGL/MG_Remote/Server/PipeApplier.cpp:104, :111, :116, :121, :132, :144, :155, :168, :209, :239, :255, :349, :354, :359, :618, :745, :1066 - seventeen more server-side funnel Fatals on peer fence / clear / readback / draw / stream-output records. A7-11 covers only six PipeApplier std::abort sites (:333, :1156, :1176, :1182, :562, :571) and none of these seventeen, because the census grepped `std::abort` and these route through Wire::WireProtocolFatal.
- MobileGL/MG_Remote/Transport/ReplySlot.h:196, :227, :232, :239 and MobileGL/MG_Remote/Transport/Ring.cpp:370 - the five WireLogFatal call sites. Every one is a death site that carries a Fatal{ tag, and NONE is visible to the `std::abort` grep the whole census is built on. The item's c6 ask #4 asks the right question (count aborts, or Fatal{ tags, or both?) but the item's own answer - 92 statements / 91 reachable - only works for the abort grep. The two greps disagree by far more than the two sites the item names: ~98 extra Fatal{-tagged death sites (93 WireProtocolFatal/At + 5 WireLogFatal) are invisible to it.
- NEGATIVE RESULT the item should have recorded, and the direct answer to A7-19's natural_carrier: MobileGL/MG_Remote/CapsCodec.cpp:318 (`count > r.RoomFor(sizeof(Int32))`) and :426 (`extensionCount > r.RoomFor(sizeof(Uint32))`) already do exactly what A7-19 asks ProgramArtifactsCodec to do - bound the count by remaining-bytes-divided-by-element-size BEFORE the resize at :324 and :430. RoomFor is defined at MobileGL/MG_Remote/CapsCodec.cpp:162-164 as `m_left / elementBytes`. This is the in-tree model for the fix and neither A7-19 nor any c6 ask cites it.
- NEGATIVE RESULT worth recording so c6 does not re-derive it: MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1948 `overrideNames.reserve(static_cast<SizeT>(count))` is SAFE. count is `layout.TailBytes[2] / sizeof(MGPProgramStorageOverride)` at :1947 - a division, not a multiplication - and TailBytes is bound by the tail cross-check at :1511-1521. Same shape as A7-25's MultiDraw negative result, same load-bearing guard.
- The design contradiction behind A7-29 and c6 ask #6: MobileGL/MG_Remote/Transport/ShmSegment.h:62-63 states the peer gets SEG_CMD read-only on the server side, but the RingControl page lives inside SEG_CMD (MobileGL/MG_Remote/Transport/SessionRings.h:165) and the server writes four of the five watermarks into it (SessionRings.h:439, Ring.cpp:593, :637, :645). The header's stated P6 design cannot be implemented as written. Neither A7-8 nor A7-29 notices, and c6 ask #6 asks the question in a form that presumes the design works.
---
## a6.8 窗口句柄与分片
**小结**:PART A: there are exactly FOUR store statements to `m_windowHandle` in the whole tree (`BackendObject.cpp:475`, `:365`, `:206`, `:207`) and exactly FIVE reads (`BackendObject_DirectGLES.cpp:901`, `:963`, `:964`, `BackendObject_DirectVulkan.cpp:338`, `:343`). The corpus's "SetWindowHandle is the sole injection point" is false in the way the integrator said, but the fix is smaller than the review implies: the only full store, `SetWindowHandle` (`:473-475`), is reached from TWO callers - the wire op (`ServerLoop.cpp:1016`) and `ActivateEGLSurface` (`:283`) - and `ActivateEGLSurface` takes its value from `m_eglSurfaces[surface].Window`, written at exactly one place, `RegisterEGLWindowSurface` (`:232-238`). So a server-owned window needs ONE substitution at `:235`, ONE refusal at `ServerLoop.cpp:1015-1017`, ONE preserve in `ResetEGLRuntimeState` (`:365`), and - the non-obvious fourth - a new dedup key for `BackendObject_DirectGLES.cpp:962-964`, whose `sameHandle` compares the CLIENT's window pointer against the server's latched one and would therefore destroy and rebuild the native context on every `eglCreateWindowSurface`. The `Width`/`Height` writes at `:206-207` clobber nothing today because NOTHING READS THEM: `m_windowHandle.Width`/`.Height` have zero readers at this head. PART B: `resource_subdata`/`buffer_subdata_resident` (buffer ranges), texture levels (three-arm slab walk, not just whole-width), the persistent-map push and `GetTextureImage` all HAVE chunking walks; `create_shader_state`'s reflection+SPIR-V archive, `set_global_constants`' whole default UBO, `set_program_bindings`' three tails, `draw_vbo`'s `MGPDrawRange[NumDraws]` tail and `read_pixels`' single reply slot DO NOT. The corpus's "host index span" is wrong: `kDrawHasUserIndices` has NO producer at this head - the only occurrence in an emitter is the CLEAR at `OwnedDrawInputs.h:51` - so the largest unchunked client->server blob is the program archive and the largest unchunked TAIL is a multi-draw range array. The one thing c6 most needs: a stream link window must be sized against `create_shader_state`'s archive (the only unbounded blob) and `draw_vbo`'s range tail (the only unbounded tail), not against the index span the corpus names.
| A8-1 | p6-must-handle | `m_windowHandle = handle;` - THE ONLY full store to the window handle in the tree | MobileGL/MG_Backend/BackendObject.cpp:473-475 (BackendObject::SetWindowHandle) | the `void*` is an ANativeWindow*/HWND/XID in the CLIENT process. The server's copy would name client memory. Nothing in the server can dereference it | SurfaceOpCodec.cpp:161 / :202 `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}` and :165 / :208 `MetalLayer@P12` - b | cp | |
| A8-2 | p6-must-handle | `backend->SetWindowHandle(UnpackWindowHandle(frame))` - the wire seam the corpus calls the SOLE injection point | MobileGL/MG_Remote/Server/ServerLoop.cpp:1015-1017 (ApplySurfaceControlFrame, case SurfaceControlOp::SetWindowHandle); unpacker at :875-882; only poster is ServerLoop.cpp:1173-1178 (ServerSetWindowHandle), whose only caller is MG_Remote/Client/BackendObject_Remote.cpp:215 | the token is a foreign address. It IS a seam a server-owned window must refuse - but deleting it is NOT sufficient, because A8-3 re-injects the same value | WindowBackendFromFrameValue's Fatal{ProtocolCorruption,"SurfaceOp.windowBackend"} (ServerLoop.cpp:864-873) covers a bad | cp | |
| A8-3 | blocks-p6 | `SetWindowHandle(surfaceState->Window)` - THE SECOND SEAM the corpus missed | MobileGL/MG_Backend/BackendObject.cpp:282-283 (BackendObject::ActivateEGLSurface, Window arm) | with the server owning its own window, this overwrites the server's latched handle with the client-supplied copy held in m_eglSurfaces, on every activation - including both MakeEGLCurrent activation paths, which have no SetWindowHandle op in front of them at all | none | P12 | **wrong-verdict** — Demote A8-3 to `p6-must-handle` at phase P12, matching A8-4/A8-5/A8-6/A8-7/A8-9/A8-11/A8-15. Reserve `blocks-p6` for the rows that genuinely gate spawn bring-up: A8-13 (the refusal's side of the wire) |
| A8-4 | p6-must-handle | `.Window = handle` - the ONLY write to the per-surface copy that A8-3 re-derives from; the smallest latch point | MobileGL/MG_Backend/BackendObject.cpp:232-238 (BackendObject::RegisterEGLWindowSurface); field declared at BackendObject.h:632 | holds the client's window pointer for the life of the surface, so every later activation re-clobbers | RegisterEGLWindowSurface refuses `handle.Backend == Unknown // !handle.Handle` (:227-229) - a null/untagged handle, not | P12 | |
| A8-5 | p6-must-handle | MakeEGLCurrent path 1: first activation when no surface is initialised - reaches A8-3 with no SetWindowHandle op in front of it | MobileGL/MG_Backend/BackendObject.cpp:317-322 (`if (!m_eglSurfaceInitialized) { if (draw != read // !ActivateEGLSurface(draw)) ... }`) | a window handle reaches the driver through a path the wire's SetWindowHandle op never touched - so a P12 design that only guards the wire op has a hole here | none | P12 | |
| A8-6 | p6-must-handle | MakeEGLCurrent path 2: switching the current surface - the second no-op-free route to A8-3 | MobileGL/MG_Backend/BackendObject.cpp:332 (`if (draw != m_eglSurface && !ActivateEGLSurface(draw))`) | same as A8-5; ActivateEGLSurface's early-out at :278-280 only covers the identical-surface repeat | none | P12 | |
| A8-7 | p6-must-handle | `m_windowHandle = {};` - the ZEROING clobber | MobileGL/MG_Backend/BackendObject.cpp:359-367 (ResetEGLRuntimeState), reached from BackendObject.cpp:429 (DestroyPendingEGLSurfaceIfUnused), :461 (ReleaseEGLSurface), :467 (ReleaseEGLResources), BackendObject_DirectGLES.cpp:969-972 (destroy-recreate inside CreateEGLWindowSurface) and :1000-1004 (inside CreateEGLPbufferSurface) | a server-owned window is zeroed by a client-driven release or by a pbuffer create, and the next InitWindowSurface (A8-9/A8-11) then builds on a null window. DirectVulkan's InitWindowSurface names that refusal (`:338-341`); DirectGLES does not check at all | BackendObject_DirectVulkan.cpp:338-341 logs and returns false on a null handle. DirectGLES has no equivalent | P12 | |
| A8-8 | later-stage | `m_windowHandle.Width/.Height = width/height` - the geometry writes, and the finding that NOTHING READS THEM | MobileGL/MG_Backend/BackendObject.cpp:205-208 (ResizeEGLWindowSurface, gated on `m_eglSurface == surface`); the surface-state twin at :203-204; reached from EGLImpl.cpp:734, ServerLoop.cpp:922-925, BackendObject_Remote.cpp:234, BackendObject_DirectVulkan.cpp:430 | nothing - these two stores are dead today. They become a clobber only on the day the server owns its own extent, which is exactly P12 | n/a | P12 | |
| A8-9 | p6-must-handle | reader 1: `reinterpret_cast<NativeWindowType>(m_windowHandle.Handle)` -> DirectGLES::InitWindowSurface(nativeWindow) | MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:899-906 | dereferences a client-process ANativeWindow*. No null check and no tag check - a garbage pointer reaches eglCreateWindowSurface | none at this site; only the wire decoder's Fatal (A8-1) | P12 | |
| A8-10 | p6-must-handle | reader 2: the `sameHandle` dedup key compares the INCOMING client handle against the latched one - the fourth edit point a server-owned window forces | MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:962-967, with the destroy-recreate it gates at :969-972 | with the server owning the window these NEVER match, so `sameHandle` is always false and every create runs DestroyEGLContext + ResetEGLRuntimeState - a full native context rebuild per surface create, plus the A8-7 zeroing. The dedup key must become the client's SURFACE token, not the window pointer | none - it degrades silently into a rebuild, which is a performance and lifetime fault, not a refusal | P12 | |
| A8-11 | p6-must-handle | reader 3: DirectVulkan's InitWindowSurface null-checks then casts the handle | MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:337-352 | same as A8-9. Note the null check (`:338`) is the ONLY guard of its kind in either backend, and it catches A8-7's zeroing but not a foreign pointer | MGLOG_E + return false at :339-341 | P12 | |
| A8-12 | no-action | the CLIENT's own m_windowHandle instance (BackendObject_Remote inherits the base) is WRITE-ONLY | MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:210-226 (CreateEGLWindowSurface calls the base at :225, which reaches BackendObject.cpp:283 -> :475) and :145-150 (InitWindowSurface is a no-op that reads nothing) | nothing. The client's copy is harmless dead state and needs no P6 or P12 work | n/a | P6.5 | |
| A8-13 | blocks-p6 | the AndroidNativeWindow@P12 refusal fires in the SERVER process, after the record has crossed; the client emits and the ENCODER does not refuse | encoder: MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:130-136 (EncodeSurfaceOpFrame) + :99-108 (WireWindowKindForWindowBackend maps Android -> AndroidNativeWindow and returns true). decoder refusal: :159-166, :199-213. client emitter with no pre-check: MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:214-216 | on the rooted Redmi the first eglCreateWindowSurface from FCL/Minecraft will `std::abort()` the SERVER process. The CLIENT sees a dead peer / EOF, not a named refusal in its own log. The discipline says 'always a NAMED refusal'; here the name is in the corpse | the Fatal exists (SurfaceOpCodec.cpp:202-206) but is on the wrong side of the wire for diagnosis | c6 | **overstated** — Keep the verdict blocks-p6 - the decision is real and c6 must make it - but restate the mechanism: at b3b9c2ee the wire surface-op codec is reachable only from unit tests, so the Fatal has never run a |
| A8-14 | already-handled | the two client-side WindowHandle construction sites and the compile-time backend tag | MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:135-140 (CreateWindowSurface) and :692-697 (CreatePlatformWindowSurface); DetectWindowBackend at :59-71 | DetectWindowBackend is `#if defined(__ANDROID__)` - it answers for the CLIENT's compile target and is carried as truth about a window the SERVER will try to use. Under spawn both processes are the same build so the tag is right; it is the POINTER beside it that is wrong | the decode-side refusal (A8-13) | P6.5 | |
| A8-15 | p6-must-handle | the release paths that reach the zeroing store | MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:313-315 (DestroySurface -> ReleaseEGLSurface) and :326-329 (Terminate -> ReleaseEGLResources); forwarded at BackendObject_Remote.cpp:300-311; dispatched at ServerLoop.cpp:986-1010 | a client-driven eglTerminate zeroes the server's window handle (A8-7) and tears down the server's context. With a server-owned window whose lifetime is the Service's (P12), the client must not be able to zero it | the blocking-by-contract handshake preserves ORDER, not ownership | P12 | |
| B8-1 | already-handled | resource_subdata / buffer_subdata_resident: CHUNKED. The walk cuts at MGPipeStageChunkBytes() = clamp(stage/4, 4096, stage) = 8 MiB by default | MobileGL/MG_Impl/Pipe/PipeFill.cpp:1011-1017 (MGPipeContentChunkCap), :1020-1052, :1054-1085; walk at MobileGL/MG_Impl/Pipe/ResourceTracker.h:250-267; budget at MobileGL/MG_Remote/Client/GpuWritePending.cpp:205-233 | nothing | PipeWireCodec.cpp:855-864 and :918-927 Fatal{RingOverrun, "SEG_STAGE"} as the backstop | P8 | |
| B8-2 | already-handled | texture level upload: CHUNKED, and with THREE arms, not just whole-width slabs - depth-slices, then whole-row runs, then sub-row texel runs | MobileGL/MG_Impl/Pipe/TextureEmit.h:446-504 (MGPipeForEachTextureSlab; :472-479 slices, :480-491 rows, :492-503 texels), driven at :1499-1517 | nothing | same SEG_STAGE Fatal | P8 | |
| B8-3 | p6-must-handle | the texture walk's SILENT FALLBACK to an uncut whole-level record when the pitch does not tile the level exactly | MobileGL/MG_Impl/Pipe/TextureEmit.h:457-459 (`if (sliceStride * depth != levelBytes) return false;`) and the caller's `if (!split)` arm at :1518-1531 | a level larger than the stage arena that also fails the tiling test falls back to one whole blob and dies at PipeWireCodec.cpp:858 Fatal{RingOverrun,"SEG_STAGE"} with a message that says 'report the record type with no cut' - which is wrong here: the type HAS a cut and declined to use it | the SEG_STAGE Fatal fires, but names the wrong cause. There is no log line at the `return false` itself | P8 | |
| B8-4 | p6-must-handle | create_shader_state: NO CHUNKING WALK. The whole LinkArtifacts + SpirvArtifacts archive (up to 6 SPIR-V modules) is staged as ONE blob | MobileGL/MG_Remote/Client/WireTables.cpp:514-559, staged whole at :556 (`record.Reflection = session.Encoder().StageBytes(archive.data(), archive.size())`); catalogue row MobileGL/MG_Pipe/PipeCalls.def:201; payload MobileGL/MG_Pipe/MGPipeTypes.h:610-641 | unchanged mechanically, but it is the LARGEST unbounded client->server blob and therefore the number a stream link window must be sized against. Worst case = sizeof(all six generatedSpirv modules) + the reflection archive; there is no ceiling in the code and no measurement of the distribution (MEASUREMENTS.md:342 says the post-chunking per-blob byte distribution is unmeasured) | PipeWireCodec.cpp:855-864 Fatal{RingOverrun,"SEG_STAGE"} at stage capacity (32 MiB default); WireTables.cpp:552-555 Fata | P8 | **line-does-not-resolve** — Fatal{ArchiveEmpty} is at WireTables.cpp:549-552 (MGLOG_F at :550). The six per-stage blobrefs are MGPipeTypes.h:635 (`MGPBlobRef Spirv[6]; // per stage`), zeroed at WireTables.cpp:555. Note also that |
| B8-5 | later-stage | set_global_constants: NO CHUNKING WALK. The whole default uniform block is one blob | MobileGL/MG_Impl/Pipe/ProgramEmit.h:202-241 (size from `program->GetUBOSize()` at :209, routed with that size at :233-234); staged via MobileGL/MG_Remote/Client/WireTables.cpp:221 (MGP_WIRE_BLOB); catalogue MobileGL/MG_Pipe/PipeCalls.def:215; payload MGPipeTypes.h:1092-1099 | unchanged mechanically. Realistic worst case is bounded by what the shader compiler produced for the default block - practically <= 64 KiB (GL_MAX_UNIFORM_BLOCK_SIZE's floor) - so it is far below the program archive and should NOT drive the link window size, contrary to the corpus which names it alongside the index span | the SEG_STAGE Fatal | P8 | |
| B8-6 | no-action | set_program_bindings: NO CHUNKING WALK on its three tails; the NAMES are staged one at a time so each name is small, but the three tails are one reservation | MobileGL/MG_Remote/Client/WireTables.cpp:575-625 (per-name StageBytes at :597, the three tails at :611-621, emitted at :622-623); catalogue MobileGL/MG_Pipe/PipeCalls.def:340; header MGPipeTypes.h:1077-1085 | nothing mechanically; the tails are bounded by reflection, not by application data | the record bound, PipeWireCodec.cpp:1019-1031 Fatal{RingOverrun,"<op>"} at MaxRecordBytes (half the SEG_CMD ring, 4 MiB | P8 | |
| B8-7 | p6-must-handle | draw_vbo's MGPDrawRange[NumDraws] tail: NO CHUNKING WALK. The largest unbounded TAIL in the catalogue | MobileGL/MG_Remote/Client/EmitTables.cpp:536-545 (`tails[0] = {ranges, numDraws * sizeof(MGPDrawRange)}`); MGPDrawRange is 12 bytes (MGPipeTypes.h:1620-1624); catalogue PipeCalls.def:261 | unchanged mechanically, but this is the ONE hot-path record whose size is application-controlled and unchunked. A glMultiDrawElements with ~350k sub-draws exceeds a default 4 MiB MaxRecordBytes. MEASUREMENTS.md:340 itself flags 'oversized DrawVbo tail' as a named unit refusal and :133 says the index/indirect tails 'still must be recorded' | PipeWireCodec.cpp:1019-1031 Fatal{RingOverrun,"DrawVbo"} - a named refusal, with no cut | P8 | **line-does-not-resolve** — Drop the MEASUREMENTS.md:133 cite - it names nothing. Keep MEASUREMENTS.md:340, which carries the whole load by itself. In the `open` block, the measurement instrument is at EmitTables.cpp:1067-1073 ( |
| B8-8 | already-handled | CORRECTION: draw_vbo's host index span (kDrawHasUserIndices MGHostSpan) has NO PRODUCER at this head - the corpus names it as a remaining unchunked type and that is wrong | the only occurrence of kDrawHasUserIndices in any emitter is the CLEAR at MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h:51; client index arrays become owned buffer resources at :46-56 and travel as ordinary chunked resource_subdata. EmitTables.cpp:536-543 only ever fills tails[1] with MGPDrawIndirect. The decode-side path (PipeWireCodec.cpp:2157-2178, :565-585) is live but unreachable | nothing - there are no host spans on the wire at all (kCapNeedsHostIndexBytes/kCapNeedsHostUboBytes are 0 for the whole of P5 by ruling, MG_Backend/Init.cpp:171-173) | CheckHostSpanIsHonest's four arms (PipeWireCodec.cpp:542-563) including the segment-range arm, plus CheckDrawUserIndices | P8 | |
| B8-9 | already-handled | set_shader_buffers' OPTIONAL second host-span tail: no producer | MobileGL/MG_Remote/Client/WireTables.cpp:227-232 (the generic MGP_WIRE_TAIL carries the FIRST tail only, and says so); the bit that would ask for it is kCapNeedsHostUboBytes, never published (MG_Backend/Init.cpp:171-173, :184) | nothing | the cap-bit ruling | P7 | |
| B8-10 | no-action | the six structurally bounded blobs with no chunking walk and no need for one | MobileGL/MG_Pipe/PipeCalls.def:187 CreateRenderState (RenderStateParameters chunk bytes, <= 1168 B - MGPipeTypes.h:471-478), :190 CreateVertexElements (MGPVertexAttribWire[<=32] + binding points - :522-532), :197 CreateSamplerState (SamplerParameters POD - :539), :205 SetDynamicState (dynamic chunks of the same 1168 B, staged OPTIONALLY - WireTables.cpp:255-268), :224 SetResidualValueState (MGL_RESIDUAL_BLOCK_SIZE == 8 - MGPipeTypes.h:1185-1197), :297 SetStorageBlockBinding (one NUL-terminated block name) | nothing | the SEG_STAGE Fatal as a backstop | P8 | **line-does-not-resolve** — Cite MG_Pipe/MGPipeValueTypes.h:660 for the 1168-byte bound (or MGPipeRenderStateSpans.h:36 for the span layout); MGPipeTypes.h:471-478 only establishes the 48-byte record. Downgrade the '<=32' attrib |
| B8-11 | p6-must-handle | server->client bulk: read_pixels is UNCHUNKED (one SEG_REPLY slot); get_texture_image IS chunked; buffer writeback IS chunked | read_pixels: MobileGL/MG_Remote/Transport/ReplySlot.h:171-207 (RequireReadPixelsFits -> Fatal{ReplyTooLarge}), slot cap = ReplyBytes/SlotCount = 16 MiB/8 = 2 MiB (SessionRings.h:91-97, kDefaultReplySlotCount = ReplySlot.h:105), emitter check at EmitTables.cpp:955. get_texture_image: MobileGL/MG_Remote/Client/TextureReadbackEmit.inc:84-95 slices at session.MaxReplyBytes(). buffer writeback: MobileGL/MG_Remote/Client/GpuWritePending.cpp:189-203 slices at EventRingCapacity/4 (64 KiB by default) | a full-HD or 4K glReadPixels exceeds 2 MiB and Fatal{ReplyTooLarge} fires at the CLIENT before emission - a named refusal, correct, but a hard functional ceiling on the Redmi (1080p RGBA8 = 8.3 MB). The refusal text says this is 'a P6 debt (chunked readback), not a bigger pool' | Fatal{ReplyTooLarge} at ReplySlot.h:196 and the last-line-of-defence in ClientSession.cpp:1119-1126 | P9 | |
| B8-12 | already-handled | the persistent-map push is chunked twice: at the block granularity, then at the content cap | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:523 (block bytes = MG_Config::Ipc.PersistentBlockKb * 1024, default 64 KiB) -> :735 PushMappedSpanBlock -> MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:456-486 -> NotifySubData -> the chunked MGPipeEmitResourceSubData | nothing | both walks | P11 | |
| B8-13 | already-handled | get_caps' two blobrefs do not ride a record at all; the caps snapshot crosses the control plane as a bounded RLE table | MobileGL/MG_Remote/CapsCodec.cpp:178-248 (EncodeFormatCapabilities, RLE over kFormatCapabilityTargetCount x kFormatCapabilityFormatCount cells declared at MG_Backend/BackendObject.h:102-103); PipeCalls.def:154 carries kHasBlob but the row is answered by CapsSnapshot, not by a record (ServerLoop.cpp:1017-1030, ROADMAP.md:126) | nothing about its SIZE. ROADMAP.md:126 notes it still owes a server->client carrier RULE - it may not be squeezed into SEG_STAGE - and under spawn it already crosses as a control frame, which is that rule | the ABI fingerprint assertion (ServerSession.cpp:91-108 FatalAbiMismatch) | cp | |
| B8-14 | blocks-p6 | what bounds an ANNOUNCED segment size today: ShmSegment::Adopt's fstat check, which is skippable and has no upper bound | MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:120-141 | the guard is `if (::fstat(fd,&st) == 0 && st.st_size > 0 && st.st_size < size) refuse`. If fstat FAILS, or st_size is 0, the announced size is TRUSTED and Map() mmaps that many bytes - a read past the real end is SIGBUS, not a named refusal. There is also NO UPPER bound: a peer announcing 4 GiB with a 4 GiB memfd is adopted and mapped | MOBILEGL_ERR_INVALID_ARGUMENT with a log line at :129-133 - only in the three-way-true case | so | |
| B8-15 | blocks-p6 | the Welcome geometry cross-check has NO spawn counterpart: it compares against an in-process ServerSession | MobileGL/MG_Remote/Client/ClientSession.cpp:574-593 (`agrees()` compares welcome->cmdRing()->sizeBytes() against server.Shm().AnnouncedSize(slot), where `server` is Server::ServerSessionInstance() obtained at :534 in the same process) | there is no ServerSession object in the client process, so this whole block must be replaced. Its comment at :570-573 says the cross-check exists precisely so 'the assertion that would otherwise first run in P6' is not first exercised on the expensive day - but what it asserts (server-announced == server-mapped) is trivially true and is NOT the assertion spawn needs, which is 'announced <= what I am willing to map' | MOBILEGL_ERR_PROTOCOL_MISMATCH + Stop() at :590-593 | so | |
| B8-16 | blocks-p6 | CORRECTION: MOBILEGL_IPC_RING_MB / _STAGE_MB ceilings are 1024 / 4096 MiB as claimed, but they are consumed in the SERVER process, not the client | ceilings: MobileGL/ConfigLoader.cpp:363-364 (`QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024)`, `QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096)`). consumption: MobileGL/MG_Remote/Server/ServerSession.cpp:120-129 (SizesFromConfig) applied at :442-445. ownership: MobileGL/MG_Remote/Transport/SessionRings.h:126-131 ('WHO CREATES THEM: the SERVER') | the CHILD's env decides. P6-CONTRACT-DRAFT.md:69-70 says 'every role-selecting MOBILEGL_IPC_* removed from the child's envp' - if the scrub is by prefix, the child falls back to 8/32 MiB while the client was launched with e.g. STAGE_MB=256, silently. ServerSession.cpp:113-119 is a written-down precedent for exactly this class of bug ('a knob that REPORTS a value it does not use is worse'). The chunking budget itself stays correct because ClientSession::StageCapacityBytes() (ClientSession.cpp:1479) reads the ATTACHED segment, not the client's own config | none - there is no cross-check that the client's requested sizes and the server's applied sizes agree, and no log line n | sm | |
**复审另行指出的遗漏**:
- MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:955-960 - the DirectGLES backend's own named refusal on the INCOMING WindowHandle (`handle.Backend` allow-list of Android/X11/MetalLayer/Win32, plus `!handle.Handle`), MGLOG_E at :958 and `return false` at :959. It sits four lines above the `sameHandle` dedup key A8-10 names (:962-967) and A8-10 never mentions it. It matters for c6_ask #2 and #4: under P12 option (b) - substitute the server's window once at RegisterEGLWindowSurface (BackendObject.cpp:235) - this guard still inspects the CLIENT's handle, so a P12 design that stops shipping a client window handle at all makes this arm refuse every create unless it is re-keyed or moved.
- MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:415-419 - the DirectVulkan twin of the same incoming-handle refusal (MGLOG_E at :417), which runs BEFORE the RegisterEGLWindowSurface call at :421. A8-4 cites the enclosing :409-422 as 'registers WITHOUT activating' but never names the guard inside it, so an integrator reading A8-4 alone would not know that Magma's create path has a second client-handle gate ahead of the latch point the audit recommends substituting at.
- MobileGL/MG_Backend/BackendObject.cpp:180 (`m_eglDisplay = dpy;`) and :469 (`m_eglDisplay = EGL_NO_DISPLAY;`), field declared at MG_Backend/BackendObject.h:649 - one line below m_windowHandle at :648. This is the EGLDisplay twin of the whole A-family and PART A never mentions it. I checked it: the client's EGLDisplay is latched in the server's backend and is NEVER dereferenced - the only uses are equality comparisons (InitializeEGLDisplay :175, MakeEGLCurrent :315, SwapEGLBuffers :385) - exactly the write-only shape A8-12 found on the client's own m_windowHandle. That is a POSITIVE finding c6 needs: of the EGL handles the server backend latches, the window is the only one that must migrate under P12, and the audit's claim to have covered the family is incomplete without saying so.
- MobileGL/MG_Pipe/PipeCalls.def:207, :210, :211, :212, :214, :216 - six kVarTail catalogue rows (SetVertexBuffers, SetSamplerViews, BindSamplerStates, SetShaderImages, SetStreamOutputTargets, SetVertexAttribDefaults) that PART B never examines, although the item's stated scope is 'every record type with no chunking walk'. None has one. B8-10 enumerates only the kHasBlob rows and B8-6/B8-7 only two tails, so six of the catalogue's ten tail-bearing rows go unnamed. I checked each: all carry the bounded `Start, Count` binding-range shape (MGPipeTypes.h:868-869 MGPVertexBuffers, :916-917 MGPSamplerViews, :923-924 MGPSamplerStates, :940 MGPShaderImages, :1029 MGPStreamOutputTargets, :1125 MGPVertexAttribDefaults) and are bounded by GL binding-point limits, so every verdict is no-action - but the enumeration as written is not the exhaustive sweep the summary claims, and c6 should not read 'the only unbounded tail is draw_vbo's' as having been proved across all ten.
- MobileGL/MG_Pipe/PipeCalls.def:227 - ResourceSubData carries `kHasBlob|kVarTail|kReplySlot`, and B8-1 covers only the blob half. Its var tail is MGPSubRegion[], sized from the dirty-rect count at MG_Impl/Pipe/TextureEmit.h:1506 (`m_regions.assign(std::max<SizeT>(rectCount, 1), MGPSubRegion{})`) and :1525-1527, and critically the slab walk does NOT cut it: each slab piece re-derives up to rectCount regions (TextureEmit.h:1506-1509), so the region tail rides every piece uncut. I chased the bound and it holds - MipmapStorage::kMaxDirtyRects = 96 at MG_State/GLState/TextureState/MipmapStorage.h:111, enforced at MipmapStorage.cpp:399 and read with that cap at TextureEmit.h:1421-1425 - so this is a no-action. But B8-2 as written would let an integrator conclude the texture path is fully cut when one of its two carriers is not cut at all and is bounded only by a constant in another subsystem.
- MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:850 - a SECOND `buffer.PushMappedSpanBlock(...)` call site. B8-12 names only :735. Both funnel into MG_State/GLState/BufferState/BufferObject.cpp:456 -> NotifySubData -> the chunked MGPipeEmitResourceSubData, so the already-handled verdict is unchanged, but the row's 'the persistent-map push' is two producers, not one, and a c6 reader auditing that path would miss one of them.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.