diff --git a/CMakeLists.txt b/CMakeLists.txt index b029d34e..43e0a468 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -595,6 +595,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED) MobileGL/MG_Remote/Server/ServerSession.cpp MobileGL/MG_Remote/Server/PipeApplier.cpp MobileGL/MG_Remote/Server/ServerLoop.cpp + # P5f fc: the value frame that replaced the control mailbox's function-pointer payload, + # and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam). + MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp + MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp ) endif() diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 63c4a9af..e6bb6114 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -221,7 +221,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // EmitObjectDeathRecord: no handle means the server never saw the object and // NOTHING crosses (which replaces the mailbox's unconditional delivery), and a // handle means the record's EmitAndWait orders the death against in-flight verbs - // that name it - the one property the blocking RunOnApplyThread hop provided and + // that name it - the one property the blocking control-channel hop provided and // the only one P5c keeps. The sink (ServerVerbSink::OnObjectDeath) releases the // kind's twin table by handle on the apply thread, so neither a lifetime-id probe // nor the client's allocator is touched from there any more. diff --git a/MobileGL/MG_Remote/CONTRACT-P5E.md b/MobileGL/MG_Remote/CONTRACT-P5E.md index 658cc00a..241f3267 100644 --- a/MobileGL/MG_Remote/CONTRACT-P5E.md +++ b/MobileGL/MG_Remote/CONTRACT-P5E.md @@ -175,8 +175,8 @@ bell), the four EGL RPC returns (`BackendObject_Remote.cpp:200-245`), `Stop`. Fo `MapBuffer(READ)` / `GetBufferSubData` / a `CopyBufferSubData` source → `SyncGpuWrites` (reply row, unchanged); `glFinish` → `WaitForApplied(LastPublishedSeq)` + drain (a new client `Finish` slot; `Flush`/`Finish` stay no-ops on the wire, ARCHITECTURE:415); every `Server*` EGL forwarder -waits `WaitForApplied(LastPublishedSeq)` BEFORE the RPC (the mailbox is pumped between drain -batches, `ServerLoop.cpp:385-390`, so a make-current would otherwise land between two run-ahead +waits `WaitForApplied(LastPublishedSeq)` BEFORE the RPC (the control channel is pumped between +drain batches, `ServerLoop.cpp:400-404`, so a make-current would otherwise land between two run-ahead records); `ServerSwapEGLBuffers` is not on that list — present is the swap. **`glGetError` relaxation:** a run-ahead verb's `kEventGlError` is observed at the next drain point, i.e. at most one present credit later than the call after it; P5C §4.2 already assigns ordering to P9 and P5e @@ -190,7 +190,7 @@ push (`PersistentMapTracker.cpp:735, 850`) fire-and-forget; the TEXTURE half kee On a run-ahead server `Reserve == nullptr` is NOT `Fatal{EventRingOverflow}`: the producer latches `eventRingFull` (exists, `EventRing.h:135-143`), publishes, rings, and `ApplyThreadMain` parks at -the record boundary with `eventRingFull == 0` added to `ready` (`ServerLoop.cpp:380-383`). The +the record boundary with `eventRingFull == 0` added to `ready` (`ServerLoop.cpp:391-398`). The client re-checks `m_events.RingIsFull()` at every park exit and drains before re-parking. Deadlock argument: the server blocks only on the event ring; the client blocks only on watermarks the server advances; every client wait drains; so at most one side is parked at any instant. The diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index c7a739aa..72b2e024 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -32,7 +32,7 @@ namespace MobileGL::MG_Remote::Client { // ---- the EGL seam ------------------------------------------------------------------ // // THE NINE EGL VIRTUALS CALL v1's TWELVE FORWARDERS AND NOTHING ELSE. c1 round 1 built - // its own trampolines over ServerLoop::RunOnApplyThread and ServerLoop::Backend(), + // its own trampolines over ServerLoop's control channel and ServerLoop::Backend(), // which ran the right driver call on the right thread and was still wrong, because // three of the twelve do MORE than forward: // @@ -48,11 +48,12 @@ namespace MobileGL::MG_Remote::Client { // That is the failure this seam exists to make impossible: there is no second route to // the server's EGL, so there is no route that can skip what the forwarder does. // - // The forwarders block on mgl-srv-apply themselves and run INLINE when the caller is - // already on that thread, so this file no longer needs RunOnApplyThread, a per-call - // args struct, or a null-backend check of its own - ServerBackendOrNull() inside each - // forwarder is the one place that answers "the bring-up did not complete", and it - // answers `false` rather than crashing or succeeding locally. + // The forwarders pack a SurfaceControlFrame and block on mgl-srv-apply themselves (P5f + // fc; the channel was a function-pointer mailbox before that) and run INLINE when the + // caller is already on that thread, so this file needs no frame, no per-call args + // struct, and no null-backend check of its own - the dispatch's null-backend arm is the + // one place that answers "the bring-up did not complete", and it answers `false` rather + // than crashing or succeeding locally. // CapsMirror's adoption hook. A free function because the hook is a raw function // pointer (ID-8: this can fire on a path that must not allocate), and it reaches the diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index e88c3f89..b74c8f2b 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -36,7 +36,8 @@ // and SwapEGLBuffers' route into GetBackendFunctions().Present() - and the client needs all of // it, because Present is a class-B emitter reached through exactly that route (the verb census's // trap 3: Present has zero MG_Impl call sites). So each override does BOTH: it runs the real -// EGL work on the apply thread, through v1's ServerLoop::RunOnApplyThread, and then lets the +// EGL work on the apply thread, through ServerLoop's control-frame channel (P5f fc; v1's +// function-pointer mailbox before it), and then lets the // base class keep the client-side books. // // SetEGLSwapInterval IS THE ONE THAT MUST NOT REACH THE TABLE. The base implementation diff --git a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp new file mode 100644 index 00000000..39dbebd1 --- /dev/null +++ b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp @@ -0,0 +1,215 @@ +// MobileGL - MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "SurfaceOpCodec.h" + +#include +#include +#include +#include + +#include + +namespace MobileGL::MG_Remote { + + namespace { + + using Server::SurfaceControlFrame; + using Server::SurfaceControlOp; + + // THE ALIGNMENT IS PINNED, NOT ASSUMED. The frame's first eleven values were chosen to + // agree with the wire enum so the tables below read as identity - but a schema edit that + // renumbers either side fails HERE, at compile time, instead of silently mistranslating + // op identities between two processes that both still announce the same ABI major. + static_assert(static_cast(SurfaceControlOp::InitializeDisplay) == + static_cast(::MobileGL::Wire::SurfaceOpKind::InitializeDisplay)); + static_assert(static_cast(SurfaceControlOp::CreateWindowSurface) == + static_cast(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface)); + static_assert(static_cast(SurfaceControlOp::CreatePbufferSurface) == + static_cast(::MobileGL::Wire::SurfaceOpKind::CreatePbufferSurface)); + static_assert(static_cast(SurfaceControlOp::ResizeWindowSurface) == + static_cast(::MobileGL::Wire::SurfaceOpKind::ResizeWindowSurface)); + static_assert(static_cast(SurfaceControlOp::ReleaseSurface) == + static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseSurface)); + static_assert(static_cast(SurfaceControlOp::MakeCurrent) == + static_cast(::MobileGL::Wire::SurfaceOpKind::MakeCurrent)); + static_assert(static_cast(SurfaceControlOp::ReleaseCurrent) == + static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseCurrent)); + // fc's three schema additions, append-only (protocol.fbs's own rule). + static_assert(static_cast(SurfaceControlOp::SetSwapInterval) == + static_cast(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval)); + static_assert(static_cast(SurfaceControlOp::ReleaseResources) == + static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseResources)); + static_assert(static_cast(SurfaceControlOp::SetWindowHandle) == + static_cast(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle)); + static_assert(static_cast(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 && + static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 && + static_cast(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10); + static_assert(static_cast(::MobileGL::Wire::WindowKind::MetalLayer) == 6); + + bool OpNeedsAWindowBackend(SurfaceControlOp op) { + return op == SurfaceControlOp::CreateWindowSurface || op == SurfaceControlOp::SetWindowHandle; + } + + } // namespace + + const char* SurfaceWireErrorName(SurfaceWireError error) { + switch (error) { + case SurfaceWireError::None: return "None"; + case SurfaceWireError::UnknownOpKind: return "UnknownOpKind"; + case SurfaceWireError::InprocOnlyOpOnTheWire: return "InprocOnlyOpOnTheWire"; + case SurfaceWireError::WindowKindNamesNoBackend: return "WindowKindNamesNoBackend"; + case SurfaceWireError::UnknownWindowKind: return "UnknownWindowKind"; + case SurfaceWireError::AndroidNativeWindowArrived: return "AndroidNativeWindowArrived"; + } + return ""; + } + + bool WireKindForSurfaceControlOp(SurfaceControlOp op, ::MobileGL::Wire::SurfaceOpKind* out) { + if (!Server::SurfaceControlOpHasWireKind(op)) return false; + // The static_asserts above make this cast a table lookup spelled as a conversion: every + // kind that reaches this line has a pinned, equal wire value. + *out = static_cast<::MobileGL::Wire::SurfaceOpKind>(static_cast(op)); + return true; + } + + bool SurfaceControlOpForWireKind(::MobileGL::Wire::SurfaceOpKind kind, SurfaceControlOp* out) { + if (::flatbuffers::IsOutRange(kind, ::MobileGL::Wire::SurfaceOpKind::InitializeDisplay, + ::MobileGL::Wire::SurfaceOpKind::SetWindowHandle)) { + return false; + } + const SurfaceControlOp op = static_cast(static_cast(kind)); + if (!Server::SurfaceControlOpHasWireKind(op)) return false; + *out = op; + return true; + } + + bool WireWindowKindForWindowBackend(MG_Backend::WindowBackend backend, ::MobileGL::Wire::WindowKind* out) { + switch (backend) { + case MG_Backend::WindowBackend::Android: *out = ::MobileGL::Wire::WindowKind::AndroidNativeWindow; return true; + case MG_Backend::WindowBackend::X11: *out = ::MobileGL::Wire::WindowKind::X11; return true; + case MG_Backend::WindowBackend::MetalLayer: *out = ::MobileGL::Wire::WindowKind::MetalLayer; return true; + case MG_Backend::WindowBackend::Win32: *out = ::MobileGL::Wire::WindowKind::Win32Hwnd; return true; + case MG_Backend::WindowBackend::Unknown: *out = ::MobileGL::Wire::WindowKind::None; return true; + default: return false; + } + } + + bool WindowBackendForWireWindowKind(::MobileGL::Wire::WindowKind kind, MG_Backend::WindowBackend* out) { + switch (kind) { + case ::MobileGL::Wire::WindowKind::AndroidNativeWindow: *out = MG_Backend::WindowBackend::Android; return true; + case ::MobileGL::Wire::WindowKind::X11: *out = MG_Backend::WindowBackend::X11; return true; + case ::MobileGL::Wire::WindowKind::MetalLayer: *out = MG_Backend::WindowBackend::MetalLayer; return true; + case ::MobileGL::Wire::WindowKind::Win32Hwnd: *out = MG_Backend::WindowBackend::Win32; return true; + case ::MobileGL::Wire::WindowKind::None: *out = MG_Backend::WindowBackend::Unknown; return true; + // Surfaceless and Pbuffer are surface SHAPES, not window backends: no answer. + default: return false; + } + } + + SurfaceWireError EncodeSurfaceOpFrame(const SurfaceControlFrame& frame, + flatbuffers::FlatBufferBuilder* builder) { + ::MobileGL::Wire::SurfaceOpKind kind; + if (!WireKindForSurfaceControlOp(frame.kind, &kind)) { + return frame.kind == SurfaceControlOp::None ? SurfaceWireError::UnknownOpKind + : SurfaceWireError::InprocOnlyOpOnTheWire; + } + ::MobileGL::Wire::WindowKind windowKind = ::MobileGL::Wire::WindowKind::None; + if (frame.kind == SurfaceControlOp::CreatePbufferSurface) { + // The op implies the shape; the frame carries no windowBackend for it. + windowKind = ::MobileGL::Wire::WindowKind::Pbuffer; + } else if (OpNeedsAWindowBackend(frame.kind)) { + if (!WireWindowKindForWindowBackend( + static_cast(frame.windowBackend), &windowKind)) { + return SurfaceWireError::UnknownWindowKind; + } + } + const auto op = ::MobileGL::Wire::CreateSurfaceOp( + *builder, frame.seq, kind, frame.display, frame.surface, windowKind, frame.nativeToken, + frame.width, frame.height, frame.swapInterval, frame.readSurface, frame.context); + const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope); + return SurfaceWireError::None; + } + + SurfaceWireError DecodeWireSurfaceOp(const ::MobileGL::Wire::SurfaceOp& op, SurfaceControlFrame* frame) { + SurfaceControlOp kind; + if (!SurfaceControlOpForWireKind(op.kind(), &kind)) { + return SurfaceWireError::UnknownOpKind; + } + SurfaceControlFrame decoded; + decoded.kind = kind; + decoded.seq = op.seq(); + decoded.display = op.display(); + decoded.surface = op.surface(); + decoded.readSurface = op.readSurface(); + decoded.context = op.context(); + decoded.width = op.width(); + decoded.height = op.height(); + decoded.swapInterval = op.swapInterval(); + if (OpNeedsAWindowBackend(kind)) { + if (op.windowKind() == ::MobileGL::Wire::WindowKind::AndroidNativeWindow) { + // The one refusal that is not corruption: the window kind is LEGAL and the token + // is an ANativeWindow*, which names memory in the CLIENT's process. Real window + // arrival is P12; until then this is refused by name at ServerApplyWireSurfaceOp. + return SurfaceWireError::AndroidNativeWindowArrived; + } + MG_Backend::WindowBackend backend; + if (!WindowBackendForWireWindowKind(op.windowKind(), &backend)) { + return ::flatbuffers::IsOutRange(op.windowKind(), ::MobileGL::Wire::WindowKind::None, + ::MobileGL::Wire::WindowKind::MetalLayer) + ? SurfaceWireError::UnknownWindowKind + : SurfaceWireError::WindowKindNamesNoBackend; + } + decoded.windowBackend = static_cast(backend); + decoded.nativeToken = op.nativeToken(); + } + *frame = decoded; + return SurfaceWireError::None; + } + + void EncodeSurfaceReplyFrame(const SurfaceControlFrame& frame, + flatbuffers::FlatBufferBuilder* builder) { + const auto reply = ::MobileGL::Wire::CreateSurfaceReply(*builder, frame.seq, frame.ok, frame.eglMajor, + frame.eglMinor, /*defaultFb=*/0); + const auto envelope = + ::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceReply, reply.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope); + } + + void DecodeWireSurfaceReply(const ::MobileGL::Wire::SurfaceReply& reply, SurfaceControlFrame* frame) { + frame->seq = reply.seq(); + frame->ok = reply.ok(); + frame->eglMajor = reply.eglMajor(); + frame->eglMinor = reply.eglMinor(); + } + + MobileGLResult ServerApplyWireSurfaceOp(const ::MobileGL::Wire::SurfaceOp& op, SurfaceControlFrame* replyOut) { + SurfaceControlFrame frame; + const SurfaceWireError error = DecodeWireSurfaceOp(op, &frame); + if (error != SurfaceWireError::None) { + if (error == SurfaceWireError::AndroidNativeWindowArrived) { + MGLOG_F("MGPipe: Fatal{UnmigratedSurface, \"AndroidNativeWindow@P12\"} - a wire " + "SurfaceOp (%s) named an ANativeWindow*, which is a pointer into the " + "CLIENT's process and means nothing here. Real window arrival is P12; " + "until then the spawn surface path is pbuffer/surfaceless only", + ::MobileGL::Wire::EnumNameSurfaceOpKind(op.kind())); + } else { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp\"} - a wire surface op " + "failed validation: %s (wire kind %u, window kind %u)", + SurfaceWireErrorName(error), static_cast(op.kind()), + static_cast(op.windowKind())); + } + std::abort(); + } + const MobileGLResult rc = Server::ServerLoopInstance().RunSurfaceControlFrame(frame); + if (replyOut != nullptr) *replyOut = frame; + return rc; + } + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.h b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.h new file mode 100644 index 00000000..691111bd --- /dev/null +++ b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.h @@ -0,0 +1,93 @@ +// MobileGL - MobileGL/MG_Remote/Protocol/SurfaceOpCodec.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P5f, package fc: the codec between the inproc SurfaceControlFrame and the wire's +// Wire::SurfaceOp / Wire::SurfaceReply (protocol.fbs). +// +// The frame is the inproc payload (Server/SurfaceControlFrame.h); this is the ONLY place the +// frame meets the schema. Nothing in the inproc hot path encodes - the frame crosses the one-slot +// channel by value - but the codec is what makes "the same struct encodes under spawn" a tested +// claim rather than a plan, and it owns the two schema seams that are NOT integer translations: +// +// * SurfaceControlOp <-> Wire::SurfaceOpKind: numerically aligned for the ten framed ops and +// PINNED by static_asserts in the .cpp, but mapped through an explicit table. The four +// inproc-only kinds have no wire kind and refuse to encode by name. +// * MG_Backend::WindowBackend <-> Wire::WindowKind: the two enums do not agree (WindowKind's +// Surfaceless/Pbuffer are surface shapes, not window backends; WindowBackend's MetalLayer had +// no wire enumerator until fc added it), so the mapping is a table with named failures, never +// a cast (f0-egl census, schema gap 3). +// +// ERROR SEMANTICS. Decode is pure: it answers a SurfaceWireError. The FATALS live at the server +// entry point, ServerApplyWireSurfaceOp, because "a wire op the server cannot honour" is a +// contract violation, not a data problem: AndroidNativeWindowArrived dies +// Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (the ANativeWindow* means nothing in the +// server's process; real window arrival is P12) and everything else dies +// Fatal{ProtocolCorruption, "SurfaceOp"}. P6's control pump is the intended caller; P5f's unit +// tests drive it directly. + +#pragma once +#include + +// The generated schema header (not forward declarations): flatbuffers 25 makes +// FlatBufferBuilder an alias template instantiation, which cannot be opaque-declared, and the +// Wire enums' underlying types are the schema's own - so this header is the dependency, kept +// honest by being the ONLY schema include the codec needs. +#include +#include +#include + +namespace MobileGL::MG_Backend { + enum class WindowBackend; // BackendObject.h; opaque-declared so this header stays light +} // namespace MobileGL::MG_Backend + +namespace MobileGL::MG_Remote { + + enum class SurfaceWireError { + None = 0, + UnknownOpKind, // no SurfaceControlOp for the wire kind (or None on the wire) + InprocOnlyOpOnTheWire, // a kind that may ride the inproc channel but never the wire + WindowKindNamesNoBackend,// Surfaceless/Pbuffer where a window backend is required + UnknownWindowKind, // a WindowKind the mapping table does not know + AndroidNativeWindowArrived, // -> Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} + }; + + const char* SurfaceWireErrorName(SurfaceWireError error); + + // frame.kind <-> wire kind. Both answer false (and leave *out untouched) for None, the four + // inproc-only kinds, and any wire value the schema does not define. + bool WireKindForSurfaceControlOp(Server::SurfaceControlOp op, Wire::SurfaceOpKind* out); + bool SurfaceControlOpForWireKind(Wire::SurfaceOpKind kind, Server::SurfaceControlOp* out); + + // The explicit WindowBackend <-> WindowKind table. WireWindowKindForWindowBackend also maps + // WindowBackend::Unknown to WindowKind::None; WindowBackendForWireWindowKind maps + // WindowKind::None back to Unknown and answers FALSE for Surfaceless/Pbuffer (surface shapes, + // not window backends). + bool WireWindowKindForWindowBackend(MG_Backend::WindowBackend backend, Wire::WindowKind* out); + bool WindowBackendForWireWindowKind(Wire::WindowKind kind, MG_Backend::WindowBackend* out); + + // Encode one frame as a complete CtrlEnvelope{SurfaceOp} buffer (the builder is left + // Finished). Fails BEFORE touching the builder for a kind that may not cross. + SurfaceWireError EncodeSurfaceOpFrame(const Server::SurfaceControlFrame& frame, + flatbuffers::FlatBufferBuilder* builder); + + // Validate + decode a wire SurfaceOp into a frame. Pure - it never aborts; the Fatal + // decisions belong to ServerApplyWireSurfaceOp. The reply half of the frame is reset. + SurfaceWireError DecodeWireSurfaceOp(const Wire::SurfaceOp& op, Server::SurfaceControlFrame* frame); + + // The reply direction, same shape (CtrlEnvelope{SurfaceReply}). + void EncodeSurfaceReplyFrame(const Server::SurfaceControlFrame& frame, + flatbuffers::FlatBufferBuilder* builder); + void DecodeWireSurfaceReply(const Wire::SurfaceReply& reply, Server::SurfaceControlFrame* frame); + + // The server-side wire entry: decode, then post the frame through ServerLoop's channel and + // hand the reply half back. A decode error is a NAMED Fatal (see the header block), never a + // dropped message. No transport calls this yet - P6's control pump will. + MobileGLResult ServerApplyWireSurfaceOp(const Wire::SurfaceOp& op, + Server::SurfaceControlFrame* replyOut); + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index d11f5c37..87b6e287 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -122,11 +122,14 @@ enum class SurfaceOpKind : uint8_t { ReleaseSurface = 5, MakeCurrent = 6, ReleaseCurrent = 7, + SetSwapInterval = 8, + ReleaseResources = 9, + SetWindowHandle = 10, MIN = None, - MAX = ReleaseCurrent + MAX = SetWindowHandle }; -inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[8] { +inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[11] { static const SurfaceOpKind values[] = { SurfaceOpKind::None, SurfaceOpKind::InitializeDisplay, @@ -135,13 +138,16 @@ inline const SurfaceOpKind (&EnumValuesSurfaceOpKind())[8] { SurfaceOpKind::ResizeWindowSurface, SurfaceOpKind::ReleaseSurface, SurfaceOpKind::MakeCurrent, - SurfaceOpKind::ReleaseCurrent + SurfaceOpKind::ReleaseCurrent, + SurfaceOpKind::SetSwapInterval, + SurfaceOpKind::ReleaseResources, + SurfaceOpKind::SetWindowHandle }; return values; } inline const char * const *EnumNamesSurfaceOpKind() { - static const char * const names[9] = { + static const char * const names[12] = { "None", "InitializeDisplay", "CreateWindowSurface", @@ -150,13 +156,16 @@ inline const char * const *EnumNamesSurfaceOpKind() { "ReleaseSurface", "MakeCurrent", "ReleaseCurrent", + "SetSwapInterval", + "ReleaseResources", + "SetWindowHandle", nullptr }; return names; } inline const char *EnumNameSurfaceOpKind(SurfaceOpKind e) { - if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::ReleaseCurrent)) return ""; + if (::flatbuffers::IsOutRange(e, SurfaceOpKind::None, SurfaceOpKind::SetWindowHandle)) return ""; const size_t index = static_cast(e); return EnumNamesSurfaceOpKind()[index]; } @@ -168,37 +177,40 @@ enum class WindowKind : uint8_t { Win32Hwnd = 3, Surfaceless = 4, Pbuffer = 5, + MetalLayer = 6, MIN = None, - MAX = Pbuffer + MAX = MetalLayer }; -inline const WindowKind (&EnumValuesWindowKind())[6] { +inline const WindowKind (&EnumValuesWindowKind())[7] { static const WindowKind values[] = { WindowKind::None, WindowKind::AndroidNativeWindow, WindowKind::X11, WindowKind::Win32Hwnd, WindowKind::Surfaceless, - WindowKind::Pbuffer + WindowKind::Pbuffer, + WindowKind::MetalLayer }; return values; } inline const char * const *EnumNamesWindowKind() { - static const char * const names[7] = { + static const char * const names[8] = { "None", "AndroidNativeWindow", "X11", "Win32Hwnd", "Surfaceless", "Pbuffer", + "MetalLayer", nullptr }; return names; } inline const char *EnumNameWindowKind(WindowKind e) { - if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::Pbuffer)) return ""; + if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::MetalLayer)) return ""; const size_t index = static_cast(e); return EnumNamesWindowKind()[index]; } @@ -1052,7 +1064,9 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_NATIVETOKEN = 14, VT_WIDTH = 16, VT_HEIGHT = 18, - VT_SWAPINTERVAL = 20 + VT_SWAPINTERVAL = 20, + VT_READSURFACE = 22, + VT_CONTEXT = 24 }; uint64_t seq() const { return GetField(VT_SEQ, 0); @@ -1081,6 +1095,12 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { int32_t swapInterval() const { return GetField(VT_SWAPINTERVAL, 0); } + uint64_t readSurface() const { + return GetField(VT_READSURFACE, 0); + } + uint64_t context() const { + return GetField(VT_CONTEXT, 0); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1093,6 +1113,8 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_WIDTH, 4) && VerifyField(verifier, VT_HEIGHT, 4) && VerifyField(verifier, VT_SWAPINTERVAL, 4) && + VerifyField(verifier, VT_READSURFACE, 8) && + VerifyField(verifier, VT_CONTEXT, 8) && verifier.EndTable(); } }; @@ -1128,6 +1150,12 @@ struct SurfaceOpBuilder { void add_swapInterval(int32_t swapInterval) { fbb_.AddElement(SurfaceOp::VT_SWAPINTERVAL, swapInterval, 0); } + void add_readSurface(uint64_t readSurface) { + fbb_.AddElement(SurfaceOp::VT_READSURFACE, readSurface, 0); + } + void add_context(uint64_t context) { + fbb_.AddElement(SurfaceOp::VT_CONTEXT, context, 0); + } explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -1149,8 +1177,12 @@ inline ::flatbuffers::Offset CreateSurfaceOp( uint64_t nativeToken = 0, int32_t width = 0, int32_t height = 0, - int32_t swapInterval = 0) { + int32_t swapInterval = 0, + uint64_t readSurface = 0, + uint64_t context = 0) { SurfaceOpBuilder builder_(_fbb); + builder_.add_context(context); + builder_.add_readSurface(readSurface); builder_.add_nativeToken(nativeToken); builder_.add_surface(surface); builder_.add_display(display); diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index 04402962..9fb8d2bc 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -172,6 +172,10 @@ enum SurfaceOpKind : ubyte { ReleaseSurface = 5, MakeCurrent = 6, ReleaseCurrent = 7, + // P5f (package fc) appended these three. Append-only, always: the enum's values ARE the wire. + SetSwapInterval = 8, + ReleaseResources = 9, + SetWindowHandle = 10, } enum WindowKind : ubyte { @@ -181,6 +185,10 @@ enum WindowKind : ubyte { Win32Hwnd = 3, Surfaceless = 4, Pbuffer = 5, + // P5f (package fc): WindowBackend (MG_Backend/BackendObject.h) has a fourth window backend and + // the two enums may only meet through the explicit mapping table in Protocol/SurfaceOpCodec.cpp + // - Surfaceless/Pbuffer are surface SHAPES, not window backends, and answer "no backend" there. + MetalLayer = 6, } table SurfaceOp { @@ -193,6 +201,12 @@ table SurfaceOp { width: int; height: int; swapInterval: int; + // P5f (package fc), appended (a FlatBuffers table field's id IS its vtable slot - append only): + // MakeCurrent's semantic unit is the (display, draw, read, context) four-tuple, and `surface` + // alone cannot carry it. `surface` stays the DRAW surface; `readSurface` and `context` are 0 + // ("no surface" / "no context") for every other op and for a ReleaseCurrent. + readSurface: ulong; + context: ulong; } table SurfaceReply { diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 114a9a08..748bf13e 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -216,6 +216,8 @@ namespace MobileGL::MG_Remote::Server { m_nativeBinds.store(0, std::memory_order_release); m_clientReleases.store(0, std::memory_order_release); m_makeCurrentRepublishes.store(0, std::memory_order_release); + m_controlSeq.store(0, std::memory_order_release); + m_controlFramesDispatched.store(0, std::memory_order_release); m_haveCurrentTuple = false; { const std::lock_guard lock(m_exitMutex); @@ -269,7 +271,8 @@ namespace MobileGL::MG_Remote::Server { ServerLoop::MakeCurrentOutcome ServerLoop::ApplyMakeCurrent(MG_Backend::BackendObject* backend, EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { - // Apply thread only (RunOnApplyThread put us here), so m_haveCurrentTuple/m_cur* need no + // Apply thread only (RunSurfaceControlFrame's dispatch put us here), so + // m_haveCurrentTuple/m_cur* need no // lock. The counters are atomic for the reader on the test thread. MakeCurrentOutcome outcome; const EglBindAction action = ClassifyEglMakeCurrent(m_haveCurrentTuple, m_curDpy, m_curDraw, @@ -477,7 +480,7 @@ namespace MobileGL::MG_Remote::Server { // m_controlDone with no answer possible (the bounded join has already succeeded, so there // is not even a Fatal{ApplyThreadJoinTimeout}). With the clear under the lock, a caller // either takes the lock FIRST (its request is here and answered NOT_INITIALIZED) or takes - // it AFTER (it sees !m_running under the lock in RunOnApplyThread and returns + // it AFTER (it sees !m_running under the lock in RunSurfaceControlFrame and returns // NOT_INITIALIZED without publishing). There is no third order. { const std::lock_guard lock(m_controlMutex); @@ -511,11 +514,12 @@ namespace MobileGL::MG_Remote::Server { // be: ServerLoopTest has no GL context, so CreateBackend is never called and the // ~BackendObject_DirectGLES -> ReleaseEGLResources self-post named above is never // built. Move the store above m_backend.reset() and that post misses - // RunOnApplyThread's re-entrancy arm (ServerLoop.cpp: `if (OnApplyThread()) return - // work(user);`) while m_running is still true, so it publishes into the one-slot - // mailbox and waits on m_controlDone for an answer only THIS thread could ever - // give. The integration lane - a real EGL teardown - is the only thing that would - // catch that, and it would catch it as a hang, not as a failed assertion. + // RunSurfaceControlFrame's re-entrancy arm (`if (OnApplyThread()) return + // ApplySurfaceControlFrame(frame);`) while m_running is still true, so it publishes + // into the one-slot channel and waits on m_controlDone for an answer only THIS + // thread could ever give. The integration lane - a real EGL teardown - is the only + // thing that would catch that, and it would catch it as a hang, not as a failed + // assertion. Detail::g_applyThreadKey.store(0, std::memory_order_relaxed); } m_controlDone.notify_all(); @@ -537,8 +541,7 @@ namespace MobileGL::MG_Remote::Server { // load, and everything below this line still runs under the lock exactly as before. if (!m_controlPosted.load(std::memory_order_acquire)) return false; - ControlWork work = nullptr; - void* user = nullptr; + SurfaceControlFrame frame; { const std::lock_guard lock(m_controlMutex); if (!m_controlPending) { @@ -549,17 +552,20 @@ namespace MobileGL::MG_Remote::Server { m_controlPosted.store(false, std::memory_order_release); return false; } - work = m_controlWork; - user = m_controlUser; + frame = m_controlFrame; // CLEARED WHEN THE REQUEST IS TAKEN, not when it finishes. m_controlPending stays - // true for the duration of `work` (the C2 exit block reads it), but the thread that - // would act on the shadow is THIS one and it is busy running the request; leaving + // true for the duration of the dispatch (the C2 exit block reads it), but the thread + // that would act on the shadow is THIS one and it is busy running the frame; leaving // the shadow set would only make the post-work iteration spin instead of park. m_controlPosted.store(false, std::memory_order_release); } - const MobileGLResult result = work == nullptr ? MOBILEGL_ERR_INVALID_ARGUMENT : work(user); + // The dispatch runs OUTSIDE the lock, exactly as the old work(user) did, and fills the + // frame's reply half; the reply is then published back into the slot under the lock so + // the poster's copy-out after m_controlDone sees it. + const MobileGLResult result = ApplySurfaceControlFrame(frame); { const std::lock_guard lock(m_controlMutex); + m_controlFrame = frame; m_controlResult = result; m_controlPending = false; m_controlFinished = true; @@ -642,13 +648,17 @@ namespace MobileGL::MG_Remote::Server { return applied; } - MobileGLResult ServerLoop::RunOnApplyThread(ControlWork work, void* user) { - if (work == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT; + MobileGLResult ServerLoop::RunSurfaceControlFrame(SurfaceControlFrame& frame) { + if (frame.kind == SurfaceControlOp::None) return MOBILEGL_ERR_INVALID_ARGUMENT; + // Minted here and echoed back with the reply half: inproc it is purely diagnostic; under + // spawn it is how a SurfaceReply names its op. Atomic rather than under m_callerMutex + // because the re-entrant arm below mints without taking that lock. + frame.seq = m_controlSeq.fetch_add(1, std::memory_order_acq_rel) + 1; // RE-ENTRANCY IS NOT A DEADLOCK. The teardown path posts from the apply thread itself - // ~BackendObject_DirectGLES reaches ReleaseEGLResources - and so does anything the // applier calls that wants "run this where the context is". Running inline is the // correct answer there and the only non-hanging one. - if (OnApplyThread()) return work(user); + if (OnApplyThread()) return ApplySurfaceControlFrame(frame); const std::lock_guard callerLock(m_callerMutex); std::unique_lock lock(m_controlMutex); @@ -662,35 +672,34 @@ namespace MobileGL::MG_Remote::Server { if (backendAlive) { // M-7: the backend exists but no thread owns it - ClientSession::Start refused // (ServerLoop.cpp Start's !Accepted arm) or std::thread's constructor threw. - // Running `work` inline here would issue eglMakeCurrent on the APP thread and - // stamp g_backendContextOwnerThread with it, so a lane called split would render - // correctly with the context on the wrong thread - the one outcome R-1 exists to - // make impossible. Fatal by name, never a fallback; compare CreateBackend's - // default: arm, which also refuses rather than substitutes. + // Dispatching the frame inline here would issue eglMakeCurrent on the APP thread + // and stamp g_backendContextOwnerThread with it, so a lane called split would + // render correctly with the context on the wrong thread - the one outcome R-1 + // exists to make impossible. Fatal by name, never a fallback; compare + // CreateBackend's default: arm, which also refuses rather than substitutes. MGLOG_F("MGPipe: Fatal{ApplyThreadNotRunning, \"EGL on the app thread\"} - a " - "control request reached RunOnApplyThread with the server's backend built " - "but no mgl-srv-apply thread running (ClientSession::Start failed after " - "ServerLoop::CreateBackend). Running it inline would make the context " - "current on the APP thread - the split lane's whole premise. Refusing by " - "name rather than falling back to monolith"); + "control frame reached RunSurfaceControlFrame with the server's backend " + "built but no mgl-srv-apply thread running (ClientSession::Start failed " + "after ServerLoop::CreateBackend). Dispatching it inline would make the " + "context current on the APP thread - the split lane's whole premise. " + "Refusing by name rather than falling back to monolith"); std::abort(); } - // Backend already gone (post-Stop teardown, or the pre-Init window): the forwarders' - // ServerBackendOrNull() would answer null anyway, so NOT_INITIALIZED is the honest - // result and running inline is pointless. This is the deterministic half of C2 - a - // forwarder call after the loop stopped returns NOT_INITIALIZED, it does not hang and - // it does not run on the caller. + // Backend already gone (post-Stop teardown, or the pre-Init window): the dispatch's + // null-backend arm would answer NOT_INITIALIZED anyway, so that is the honest result + // and running inline is pointless. This is the deterministic half of C2 - a forwarder + // call after the loop stopped returns NOT_INITIALIZED, it does not hang and it does + // not run on the caller. return MOBILEGL_ERR_NOT_INITIALIZED; } - m_controlWork = work; - m_controlUser = user; + m_controlFrame = frame; m_controlPending = true; m_controlFinished = false; - // THE SHADOW IS PUBLISHED LAST OF THE FOUR AND WITH A RELEASE, inside this same + // THE SHADOW IS PUBLISHED LAST OF THE THREE AND WITH A RELEASE, inside this same // critical section: the apply thread's park predicate reads ONLY this word, so an - // acquire load that returns true must also see the three fields above it. Storing it + // acquire load that returns true must also see the slot and flags above it. Storing it // first would let a pump that is already past its own fast-negative read a slot that - // does not name the work yet. + // does not hold the frame yet. m_controlPosted.store(true, std::memory_order_release); // Publish THEN ring, in that order and never the other (Doorbell.h, NotifyIfParked's // PRECONDITION block: the fence only orders what precedes it). The bell @@ -703,9 +712,25 @@ namespace MobileGL::MG_Remote::Server { m_session->ConsumerDoorbell().Notify(); } m_controlDone.wait(lock, [this] { return m_controlFinished; }); + frame = m_controlFrame; // the reply half, written by the dispatch return m_controlResult; } + MobileGLResult ServerLoop::RunProbeOnApplyThreadForTesting(ControlProbeHook hook, void* user) { + // The hook pair is published BEFORE the frame: the mailbox handshake (publish under + // m_controlMutex, shadow last, then the bell) orders these two relaxed-typed stores for + // the apply thread, which reads them inside the dispatch. + m_controlProbeHook.store(hook, std::memory_order_release); + m_controlProbeUser.store(user, std::memory_order_release); + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::ProbeForTesting; + return RunSurfaceControlFrame(frame); + } + + Uint64 ServerLoop::ControlFramesDispatched() const { + return m_controlFramesDispatched.load(std::memory_order_acquire); + } + void ServerLoop::SignalExited() { { const std::lock_guard lock(m_exitMutex); @@ -776,192 +801,328 @@ namespace MobileGL::MG_Remote::Server { } // --------------------------------------------------------------------------------- - // The twelve EGL forwarders + // The control-frame dispatch and the twelve EGL forwarders (P5f, package fc) // --------------------------------------------------------------------------------- + // + // THE MAILBOX'S PAYLOAD CHANGED SHAPE, NOT ITS PROTOCOL. The channel is still one blocking + // slot pumped between drain batches, with the shadow, the publish-then-ring doorbell and the + // C2 exit block exactly as before; what is gone is the payload - a raw function pointer (the + // op's identity) plus a void* to a stack-local Args (its arguments and out-pointers), neither + // of which means anything in another address space (P5c audit row G4). The slot now carries + // one SurfaceControlFrame BY VALUE: `kind` is the op, the scalar fields are the arguments, + // and the reply half (ok / eglMajor / eglMinor) comes back in the same value. Nine of the + // twelve forwarders map onto protocol.fbs's ten SurfaceOp kinds; the three without a wire + // kind (f0-egl's census: present is a record, capabilities are a CapsSnapshot, and + // InitWindowSurface has no caller) ride the same channel as inproc-only kinds rather than + // keeping a second, pointer-shaped path alive for dead or answered code. namespace { - // One captureless trampoline for every call: ControlWork is a raw function pointer - // plus a void*, not a std::function, because these run on the TEARDOWN path and the - // teardown path may not allocate (ID-8 exists because frontend destructors reach here - // from exit handlers). - template - MobileGLResult RunOnApply(Args& args) { - return ServerLoopInstance().RunOnApplyThread( - +[](void* user) -> MobileGLResult { return static_cast(user)->Run(); }, - &args); + // EGL handles are pointers on every platform in scope; the frame carries them as Uint64 + // tokens. Inproc the token IS the handle value bit-cast (same process, same driver), so + // the round-trip is exact and the N-3 tuple bookkeeping - which compares handle VALUES - + // is unchanged. Under spawn the client mints dense tokens instead (P6-CONTRACT-DRAFT + // table 0); the field width is already the token's. + template + Uint64 TokenFromHandle(Handle handle) { + static_assert(std::is_pointer_v, "an EGL handle is a pointer"); + return static_cast(reinterpret_cast(handle)); + } + template + Handle HandleFromToken(Uint64 token) { + static_assert(std::is_pointer_v, "an EGL handle is a pointer"); + return reinterpret_cast(static_cast(token)); } - // Null means the hook never ran or the session is already down. Every forwarder - // answers false / no-op rather than dereferencing: a client that calls eglMakeCurrent - // on a process whose split bring-up failed must see a refusal, not a crash. - MG_Backend::BackendObject* ServerBackendOrNull() { return ServerLoopInstance().Backend(); } + // WindowHandle -> frame values (the forwarder side). The frame carries the BACKEND tag, + // not the wire's WindowKind: the WindowKind mapping belongs to the wire codec + // (Protocol/SurfaceOpCodec.cpp) and must not leak into the inproc path. + void PackWindowHandle(SurfaceControlFrame& frame, const MG_Backend::WindowHandle& handle) { + frame.windowBackend = static_cast(handle.Backend); + frame.nativeToken = TokenFromHandle(handle.Handle); + frame.width = static_cast(handle.Width); + frame.height = static_cast(handle.Height); + } + + // The dispatch-side range check. Inproc the value came from the poster's own + // WindowHandle, so an out-of-range tag is a memory-corruption shape, not bad input - + // same discipline as the ring's Fatal{ProtocolCorruption} on a header the producer could + // not have written. + MG_Backend::WindowBackend WindowBackendFromFrameValue(Int value) { + if (value < static_cast(MG_Backend::WindowBackend::Unknown) || + value >= static_cast(MG_Backend::WindowBackend::WindowBackendCount)) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.windowBackend\"} - a " + "control frame carried window backend tag %d, which names no " + "WindowBackend", value); + std::abort(); + } + return static_cast(value); + } + + MG_Backend::WindowHandle UnpackWindowHandle(const SurfaceControlFrame& frame) { + MG_Backend::WindowHandle handle; + handle.Backend = WindowBackendFromFrameValue(frame.windowBackend); + handle.Handle = HandleFromToken(frame.nativeToken); + handle.Width = static_cast(frame.width); + handle.Height = static_cast(frame.height); + return handle; + } } // namespace - Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) { - struct Args { - EGLDisplay dpy; - EGLint* major; - EGLint* minor; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->InitializeEGLDisplay(dpy, major, minor); - return MOBILEGL_OK; + MobileGLResult ServerLoop::ApplySurfaceControlFrame(SurfaceControlFrame& frame) { + // The frame channel's own tally, bumped for EVERY dispatch including probes: a forwarder + // that stopped posting frames (the fc red-once revert shape) leaves this unmoved, which + // is what ServerLoopTest's AVoidForwarderCrossesAsOneDispatchedFrame reads. + m_controlFramesDispatched.fetch_add(1, std::memory_order_acq_rel); + if (frame.kind == SurfaceControlOp::ProbeForTesting) { + const ControlProbeHook hook = m_controlProbeHook.load(std::memory_order_acquire); + if (hook == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT; + return hook(m_controlProbeUser.load(std::memory_order_acquire)); + } + // Null means the hook never ran or the session is already down. Every op answers + // NOT_INITIALIZED rather than dereferencing: a client that calls eglMakeCurrent on a + // process whose split bring-up failed must see a refusal, not a crash. + MG_Backend::BackendObject* backend = m_backend.get(); + if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; + switch (frame.kind) { + case SurfaceControlOp::InitializeDisplay: { + // The old Args carried the poster's `major`/`minor` OUT-POINTERS; here they are + // reply fields. A pointer to the caller's stack is exactly what does not survive a + // process boundary, so the version numbers travel back in the frame's value. + EGLint major = 0; + EGLint minor = 0; + frame.ok = backend->InitializeEGLDisplay(HandleFromToken(frame.display), + &major, &minor); + frame.eglMajor = major; + frame.eglMinor = minor; + return MOBILEGL_OK; + } + case SurfaceControlOp::CreateWindowSurface: { + frame.ok = backend->CreateEGLWindowSurface(HandleFromToken(frame.surface), + UnpackWindowHandle(frame)); + // N-3: BackendObject_DirectGLES destroys and recreates the native context to create + // a DIFFERENT surface, so whatever tuple was bound names a dead context. + if (frame.ok) ForgetCurrentTuple(); + return MOBILEGL_OK; + } + case SurfaceControlOp::ResizeWindowSurface: + frame.ok = backend->ResizeEGLWindowSurface(HandleFromToken(frame.surface), + static_cast(frame.width), + static_cast(frame.height)); + return MOBILEGL_OK; + case SurfaceControlOp::CreatePbufferSurface: + frame.ok = backend->CreateEGLPbufferSurface(HandleFromToken(frame.surface), + frame.width, frame.height); + // N-3: as for the window surface - a (re)creation may have destroyed the context the + // held tuple named. The surface's own creation binds natively, so the client's + // make-current that follows is forwarded and deduped one layer down (ID-54). + if (frame.ok) ForgetCurrentTuple(); + return MOBILEGL_OK; + case SurfaceControlOp::MakeCurrent: { + // C7 / ID-54: the apply thread binds the native context ONCE per context lifetime + // and holds it for life. ApplyMakeCurrent forwards a bind only for a tuple it does + // not hold and treats an identical repeat as a no-op; the native call for a surface + // already current on this thread is skipped one layer down + // (BackendObject_DirectGLES's ID-54 arm), which is what makes "the owner slot is + // written once" TRUE and measured (ServerLoopTest's C7 control) rather than claimed. + const MakeCurrentOutcome outcome = + ApplyMakeCurrent(backend, HandleFromToken(frame.display), + HandleFromToken(frame.surface), + HandleFromToken(frame.readSurface), + HandleFromToken(frame.context)); + frame.ok = outcome.ok; + if (!outcome.ok || !outcome.boundNatively) return MOBILEGL_OK; + // R-12, arm (a): the caps snapshot is REPUBLISHED because InitCapabilities has now + // run for real - on every forwarded bind of a tuple this loop did not hold (the + // first, and every DIFFERENT tuple after it), and NEVER on an identical repeat + // (ID-67: a repeat is a native no-op AND publishes nothing, so the client's mirror + // generation does not move and nothing accumulates). DirectGLES has no + // OnCapsInvalidated producer at all, and c0's answer is that a SECOND arrival IS the + // invalidation - so the client's mirror is refreshed with no dev-shaped backend edit + // and with no eleventh MGPipeCallbacks slot (MGPipeCallbacks.h:56-58's static_assert + // exists to make that cost visible). Red once by republishing only on the first + // bind: the ID-67 control's different tuple reads 1 republish where 2 are required. + ServerSession* session = ServerSession::Active(); + if (session != nullptr && session->Accepted()) { + const MobileGLResult published = session->PublishCapsSnapshot(); + if (published == MOBILEGL_OK) NoteMakeCurrentRepublished(); + if (published != MOBILEGL_OK) { + MGLOG_E("MG_Remote server: the post-make-current CapsSnapshot could not be " + "published (rc=%d); the client's mirror still holds the empty " + "snapshot Accept() sent before any context existed", + static_cast(published)); + } } - } args{dpy, major, minor}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + return MOBILEGL_OK; + } + case SurfaceControlOp::ReleaseCurrent: { + // The release request's own op on the wire. It reaches the same ClientRelease arm of + // ApplyMakeCurrent as before (recorded, NOT forwarded - ID-54: the context stays + // current on this thread until ~BackendObject_DirectGLES or context loss, so + // forwarding would unbind and reinstate the off-thread degradation the phase + // removes). A client release never republishes: nothing ran that could move caps. + const MakeCurrentOutcome outcome = + ApplyMakeCurrent(backend, HandleFromToken(frame.display), + EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + frame.ok = outcome.ok; + return MOBILEGL_OK; + } + case SurfaceControlOp::SetSwapInterval: + backend->SetEGLSwapInterval(frame.swapInterval); + return MOBILEGL_OK; + case SurfaceControlOp::ReleaseSurface: { + const EGLSurface surface = HandleFromToken(frame.surface); + backend->ReleaseEGLSurface(surface); + // N-3: a released surface the held tuple names may have taken the context with it + // (BackendObject::ReleaseEGLSurface -> OnEGLSurfaceReleased -> DestroyEGLContext once + // nothing holds it current). Forgetting when the base class only DEFERRED the destroy + // costs one forwarded bind; remembering when it did not would cost a silent + // no-context-current. + ForgetCurrentTupleIfItNames(surface); + return MOBILEGL_OK; + } + case SurfaceControlOp::ReleaseResources: + // BLOCKING BY CONTRACT (the forwarder's own comment, below): EGLImpl.cpp's teardown + // walks into MobileGL::Destroy() the moment this answers. For DirectGLES this runs + // DestroyEGLContext - eglMakeCurrent(NO_SURFACE), eglDestroyContext, eglTerminate - + // on the thread that made the context current. + backend->ReleaseEGLResources(); + // N-3: DestroyEGLContext just ran; the tuple names nothing. Without this a + // destroy-recreate with the same handle values (every EGL handle on this host is + // 0x1) classified as a RepeatNoOp, bound nothing, and republished no caps. Red once + // by deleting it: ServerLoopTest's recreate control reads NativeBindCount() == 1 + // where 2 is required. + ForgetCurrentTuple(); + return MOBILEGL_OK; + case SurfaceControlOp::SetWindowHandle: + backend->SetWindowHandle(UnpackWindowHandle(frame)); + return MOBILEGL_OK; + case SurfaceControlOp::InitCapabilitiesInprocOnly: + // Inproc-only (f0-egl §4.2): on the wire the ANSWER to this op is the CapsSnapshot + // frame itself, so there is no SurfaceOp kind for it. The frame channel still has to + // carry it inproc because InitCapabilities runs GL queries and therefore belongs to + // the apply thread. + frame.ok = backend->InitCapabilities(); + if (frame.ok) { + ServerSession* session = ServerSession::Active(); + if (session != nullptr && session->Accepted()) { + (void)session->PublishCapsSnapshot(); + } + } + return MOBILEGL_OK; + case SurfaceControlOp::SwapBuffersInprocOnly: + // Inproc-only (f0-egl F8): present travels as a class-B record with its own credit; + // this forwarder has no production caller and survives for ServerLoopTest's N-3 + // recreate control, which needs a swap that is NOT a record. + frame.ok = backend->SwapEGLBuffers(HandleFromToken(frame.display), + HandleFromToken(frame.surface)); + return MOBILEGL_OK; + case SurfaceControlOp::InitWindowSurfaceInprocOnly: + // Inproc-only (f0-egl F8): no caller at all (the client's InitWindowSurface is a + // no-op); kept because deleting the free function would delete the seam the + // inproc-only rule is stated against. + frame.ok = backend->InitWindowSurface(); + return MOBILEGL_OK; + case SurfaceControlOp::None: + case SurfaceControlOp::ProbeForTesting: + // Unreachable in a correct process (None is refused at post, the probe is answered + // above the switch); a slot that still arrives here with one shares the Fatal below. + break; + } + // A kind the switch does not know never legitimately posts (RunSurfaceControlFrame + // refuses None), so reaching this line means the slot's bytes were not written by the + // poster at all - the ring's Fatal{ProtocolCorruption} discipline, one channel over. + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SurfaceOp.kind\"} - the control slot held " + "kind %d, which names no dispatchable op", static_cast(frame.kind)); + std::abort(); + } + + // --------------------------------------------------------------------------------- + // The twelve forwarders: pack the frame, post it, unpack the reply + // --------------------------------------------------------------------------------- + + Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) { + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::InitializeDisplay; + frame.display = TokenFromHandle(dpy); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + if (rc == MOBILEGL_OK) { + // Written back whenever the dispatch ran, success or not - the old out-pointers + // reached the backend directly, and a failed eglInitialize's partial writes are the + // caller's to ignore exactly as before. + if (major != nullptr) *major = frame.eglMajor; + if (minor != nullptr) *minor = frame.eglMinor; + } + return rc == MOBILEGL_OK && frame.ok; } Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle) { - struct Args { - EGLSurface surface; - const MG_Backend::WindowHandle* handle; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->CreateEGLWindowSurface(surface, *handle); - // N-3: BackendObject_DirectGLES destroys and recreates the native context to - // create a DIFFERENT surface, so whatever tuple was bound names a dead context. - if (ok) ServerLoopInstance().ForgetCurrentTuple(); - return MOBILEGL_OK; - } - } args{surface, &handle}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::CreateWindowSurface; + frame.surface = TokenFromHandle(surface); + PackWindowHandle(frame, handle); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) { - struct Args { - EGLSurface surface; - Uint32 width; - Uint32 height; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->ResizeEGLWindowSurface(surface, width, height); - return MOBILEGL_OK; - } - } args{surface, width, height}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::ResizeWindowSurface; + frame.surface = TokenFromHandle(surface); + frame.width = static_cast(width); + frame.height = static_cast(height); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) { - struct Args { - EGLSurface surface; - EGLint width; - EGLint height; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->CreateEGLPbufferSurface(surface, width, height); - // N-3: as for the window surface - a (re)creation may have destroyed the context - // the held tuple named. The surface's own creation binds natively, so the client's - // make-current that follows is forwarded and deduped one layer down (ID-54). - if (ok) ServerLoopInstance().ForgetCurrentTuple(); - return MOBILEGL_OK; - } - } args{surface, width, height}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::CreatePbufferSurface; + frame.surface = TokenFromHandle(surface); + frame.width = width; + frame.height = height; + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { - struct Args { - EGLDisplay dpy; - EGLSurface draw; - EGLSurface read; - EGLContext ctx; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - // C7 / ID-54: the apply thread binds the native context ONCE per context - // lifetime and holds it for life. ApplyMakeCurrent forwards a bind only for a - // tuple it does not hold, treats an identical repeat as a no-op, and records a - // client release-current WITHOUT unbinding; the native call for a surface already - // current on this thread is skipped one layer down (BackendObject_DirectGLES's - // ID-54 arm), which is what makes "the owner slot is written once" TRUE and - // measured (ServerLoopTest's C7 control) rather than claimed. - const ServerLoop::MakeCurrentOutcome outcome = - ServerLoopInstance().ApplyMakeCurrent(backend, dpy, draw, read, ctx); - ok = outcome.ok; - if (!ok || !outcome.boundNatively) return MOBILEGL_OK; - // R-12, arm (a): the caps snapshot is REPUBLISHED because InitCapabilities has - // now run for real - on every forwarded bind of a tuple this loop did not hold - // (the first, and every DIFFERENT tuple after it), and NEVER on an identical - // repeat (ID-67: a repeat is a native no-op AND publishes nothing, so the client's - // mirror generation does not move and nothing accumulates). DirectGLES has no - // OnCapsInvalidated producer at all, and c0's answer is that a SECOND arrival IS the - // invalidation - so the client's mirror is refreshed with no dev-shaped backend edit - // and with no eleventh MGPipeCallbacks slot (MGPipeCallbacks.h:56-58's static_assert - // exists to make that cost visible). Red once by republishing only on the first - // bind: the ID-67 control's different tuple reads 1 republish where 2 are required. - ServerSession* session = ServerSession::Active(); - if (session != nullptr && session->Accepted()) { - const MobileGLResult published = session->PublishCapsSnapshot(); - if (published == MOBILEGL_OK) ServerLoopInstance().NoteMakeCurrentRepublished(); - if (published != MOBILEGL_OK) { - MGLOG_E("MG_Remote server: the post-make-current CapsSnapshot could not " - "be published (rc=%d); the client's mirror still holds the empty " - "snapshot Accept() sent before any context existed", - static_cast(published)); - } - } - return MOBILEGL_OK; - } - } args{dpy, draw, read, ctx}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + // The release request (the three NO_* markers, ClassifyEglMakeCurrent's ClientRelease + // shape) crosses as its own op: on the wire that is what ReleaseCurrent IS + // (protocol.fbs), and the dispatch reaches the same ApplyMakeCurrent arm either way. + frame.kind = (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT) + ? SurfaceControlOp::ReleaseCurrent + : SurfaceControlOp::MakeCurrent; + frame.display = TokenFromHandle(dpy); + frame.surface = TokenFromHandle(draw); + frame.readSurface = TokenFromHandle(read); + frame.context = TokenFromHandle(ctx); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { - struct Args { - EGLDisplay dpy; - EGLSurface draw; - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->SwapEGLBuffers(dpy, draw); - return MOBILEGL_OK; - } - } args{dpy, draw}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::SwapBuffersInprocOnly; + frame.display = TokenFromHandle(dpy); + frame.surface = TokenFromHandle(draw); + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } void ServerSetEGLSwapInterval(Int interval) { - struct Args { - Int interval; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - backend->SetEGLSwapInterval(interval); - return MOBILEGL_OK; - } - } args{interval}; - (void)RunOnApply(args); + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::SetSwapInterval; + frame.swapInterval = interval; + (void)ServerLoopInstance().RunSurfaceControlFrame(frame); } void ServerReleaseEGLSurface(EGLSurface surface) { - struct Args { - EGLSurface surface; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - backend->ReleaseEGLSurface(surface); - // N-3: a released surface the held tuple names may have taken the context with - // it (BackendObject::ReleaseEGLSurface -> OnEGLSurfaceReleased -> DestroyEGLContext - // once nothing holds it current). Forgetting when the base class only DEFERRED - // the destroy costs one forwarded bind; remembering when it did not would cost a - // silent no-context-current. - ServerLoopInstance().ForgetCurrentTupleIfItNames(surface); - return MOBILEGL_OK; - } - } args{surface}; - (void)RunOnApply(args); + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::ReleaseSurface; + frame.surface = TokenFromHandle(surface); + (void)ServerLoopInstance().RunSurfaceControlFrame(frame); } void ServerReleaseEGLResources() { @@ -970,66 +1131,32 @@ namespace MobileGL::MG_Remote::Server { // runs DestroyEGLContext - eglMakeCurrent(NO_SURFACE), eglDestroyContext, eglTerminate - // and those must happen on the thread that made the context current. A fire-and-forget // here lets Destroy() walk on while the server still holds the context, which is - // scout-install 5.3's named hazard. - struct Args { - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - backend->ReleaseEGLResources(); - // N-3: DestroyEGLContext just ran; the tuple names nothing. Without this a - // destroy-recreate with the same handle values (every EGL handle on this host - // is 0x1) classified as a RepeatNoOp, bound nothing, and republished no caps. - // Red once by deleting it: ServerLoopTest's recreate control reads - // NativeBindCount() == 1 where 2 is required. - ServerLoopInstance().ForgetCurrentTuple(); - return MOBILEGL_OK; - } - } args{}; - (void)RunOnApply(args); + // scout-install 5.3's named hazard. The frame channel is blocking, so nothing about + // that changed with fc. + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::ReleaseResources; + (void)ServerLoopInstance().RunSurfaceControlFrame(frame); } Bool ServerInitCapabilities() { - struct Args { - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->InitCapabilities(); - if (!ok) return MOBILEGL_OK; - ServerSession* session = ServerSession::Active(); - if (session != nullptr && session->Accepted()) { - (void)session->PublishCapsSnapshot(); - } - return MOBILEGL_OK; - } - } args{}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::InitCapabilitiesInprocOnly; + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } Bool ServerInitWindowSurface() { - struct Args { - Bool ok = false; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - ok = backend->InitWindowSurface(); - return MOBILEGL_OK; - } - } args{}; - return RunOnApply(args) == MOBILEGL_OK && args.ok; + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::InitWindowSurfaceInprocOnly; + const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame); + return rc == MOBILEGL_OK && frame.ok; } void ServerSetWindowHandle(const MG_Backend::WindowHandle& handle) { - struct Args { - const MG_Backend::WindowHandle* handle; - MobileGLResult Run() { - MG_Backend::BackendObject* backend = ServerBackendOrNull(); - if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED; - backend->SetWindowHandle(*handle); - return MOBILEGL_OK; - } - } args{&handle}; - (void)RunOnApply(args); + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::SetWindowHandle; + PackWindowHandle(frame, handle); + (void)ServerLoopInstance().RunSurfaceControlFrame(frame); } } // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h index 9aebf78e..f5c560c1 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.h +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -71,6 +71,7 @@ #include #include "ServerSession.h" +#include "SurfaceControlFrame.h" #include #include @@ -169,7 +170,7 @@ namespace MobileGL::MG_Remote::Server { // a type that has padding bytes would let two calls ON THE SAME THREAD return // different keys, and OnApplyThread() would then answer FALSE ON THE APPLY THREAD - // a role guard that has stopped guarding, which is the one direction CONTRACT-P5C - // rule E says a guard may never fail in (RunOnApplyThread would also lose its + // rule E says a guard may never fail in (RunSurfaceControlFrame would also lose its // re-entrancy shortcut and the EGL teardown post would deadlock on m_controlDone). // Every library this fallback is built against today wraps a single scalar // (libstdc++ __gthread_t, libc++ __libcpp_thread_id, MSVC unsigned int) and @@ -214,11 +215,29 @@ namespace MobileGL::MG_Remote::Server { // nine EGL lifecycle virtuals cross; it is deliberately NOT a queue of async messages, // because every one of them has a return value the caller acts on immediately. // - // A raw function pointer plus a user pointer, not std::function: this runs on the - // teardown path too, and the teardown path may not allocate - ID-8's leak-at-exit rule - // exists because frontend destructors reach here from exit handlers. - using ControlWork = MobileGLResult (*)(void* user); - MobileGLResult RunOnApplyThread(ControlWork work, void* user); + // THE REQUEST IS A VALUE FRAME (P5f, package fc). It used to be a raw function pointer + // plus a void* to a stack-local Args struct - neither has a meaning across a process + // boundary, which is the whole of P5c audit row G4. Now the slot carries one + // SurfaceControlFrame BY VALUE: the op's identity is the frame's `kind`, its arguments + // are scalars, and its reply fields come back in the same value when the wait returns. + // What did NOT move: the blocking handshake, the one slot, and the apply-thread + // ownership. Under spawn the same frame is what SurfaceOpCodec encodes into a + // Wire::SurfaceOp - this signature is the seam both transports share. + MobileGLResult RunSurfaceControlFrame(SurfaceControlFrame& frame); + + // The TEST seam through the same channel: posts a ProbeForTesting frame whose dispatch + // runs the given hook on the apply thread. A raw function pointer held in a MEMBER (not + // in the mailbox slot, which carries the frame and nothing else), because the suites that + // drive guards and Fatal arms need arbitrary work on the apply thread and the teardown + // path's no-allocation rule (ID-8) forbids a std::function here. Same discipline as + // SetBeforeRetireHookForTesting. + using ControlProbeHook = MobileGLResult (*)(void* user); + MobileGLResult RunProbeOnApplyThreadForTesting(ControlProbeHook hook, void* user); + + // How many frames this loop has dispatched (every kind, probe included). Reset by + // Start(). The frame channel's own red-once handle: a forwarder that stopped posting + // frames leaves this unmoved. + Uint64 ControlFramesDispatched() const; // ---- v1's additions beyond c0's signature block --------------------------------- @@ -235,8 +254,8 @@ namespace MobileGL::MG_Remote::Server { // context is never OWNED by it. MobileGLResult CreateBackend(BackendType type); - // True on the apply thread itself. RunOnApplyThread uses it to run inline rather than - // deadlock when the apply thread posts to itself - which the EGL teardown path does, + // True on the apply thread itself. RunSurfaceControlFrame uses it to run inline rather + // than deadlock when the apply thread posts to itself - which the EGL teardown path does, // because ~BackendObject_DirectGLES runs THERE and reaches ReleaseEGLResources. // // INLINE, AND THAT IS THE POINT (P5d round 3, package D): see the Detail block above. @@ -337,7 +356,7 @@ namespace MobileGL::MG_Remote::Server { // narrower than the name's "a control request is in flight". The shadow is cleared // when the pump takes the work, not when the work returns, so for the whole duration // of `work(user)` this answers false although m_controlPending is still true and the - // poster is still blocked in RunOnApplyThread. That is deliberate - it is a PARK + // poster is still blocked in RunSurfaceControlFrame. That is deliberate - it is a PARK // PREDICATE, and the thread that would act on it is the one running the work - but it // means this is not a liveness query and must not be used as one. The only in-tree // reader outside the test is the apply thread's own `ready` lambda, which by @@ -346,8 +365,12 @@ namespace MobileGL::MG_Remote::Server { private: void ApplyThreadMain(); - // Runs a posted control request, if there is one. Returns true if it ran one. + // Runs a posted control frame, if there is one. Returns true if it ran one. Bool PumpControlRequest(); + // The dispatch, on the apply thread (or inline for a re-entrant post): executes the + // frame's op against the private backend and fills the frame's reply half. The eleven + // wire+inproc op bodies from the old forwarder Args structs live here now. + MobileGLResult ApplySurfaceControlFrame(SurfaceControlFrame& frame); // Pops and applies every record currently in the ring; returns how many it applied. Uint64 DrainRing(); void SignalExited(); @@ -364,8 +387,11 @@ namespace MobileGL::MG_Remote::Server { // context - see Stop(). UniquePtr m_backend; - // The blocking control mailbox. ONE slot, because the verb barrier already leaves one + // The blocking control channel. ONE slot, because the verb barrier already leaves one // client thread runnable at a time; m_callerMutex serialises anything that is not. + // The slot carries ONE SurfaceControlFrame BY VALUE (P5f, package fc) - no function + // pointer, no address of caller storage. The poster's frame is copied in at publish and + // the dispatch's reply half is copied back over it before m_controlDone is signalled. std::mutex m_callerMutex; std::mutex m_controlMutex; // THE MAILBOX'S ONE-BIT SHADOW, and the only field of it the idle poll may read. @@ -382,11 +408,20 @@ namespace MobileGL::MG_Remote::Server { // notifier anywhere in the tree (m_controlDone is the one that carries the handshake). std::atomic m_controlPosted{false}; std::condition_variable m_controlDone; - ControlWork m_controlWork = nullptr; - void* m_controlUser = nullptr; + // The one slot. A frame by value; the reply half is written through it on the way back. + SurfaceControlFrame m_controlFrame; MobileGLResult m_controlResult = MOBILEGL_OK; Bool m_controlPending = false; Bool m_controlFinished = false; + // Minted per posted frame (0 = never posted) and the dispatch tally beside it. Both + // atomic: the re-entrant inline arm mints without taking m_callerMutex. + std::atomic m_controlSeq{0}; + std::atomic m_controlFramesDispatched{0}; + // The test probe (RunProbeOnApplyThreadForTesting). Members, NOT slot content - the slot + // carries only the frame. Written before the probe frame is published, so the mailbox's + // own release/acquire handshake orders them for the apply thread. + std::atomic m_controlProbeHook{nullptr}; + std::atomic m_controlProbeUser{nullptr}; // The BOUNDED join's other half. std::thread::join has no deadline, so a lost wakeup // would wedge CI rather than fail it; the thread signals here last and Stop() waits @@ -456,6 +491,14 @@ namespace MobileGL::MG_Remote::Server { // WHY THEY ARE FREE FUNCTIONS AND NOT MEMBERS: c1 needs exactly this surface and nothing // else of the server, so the seam between the two packages is a list of twelve signatures // rather than a class with a lifecycle. + // + // P5f (package fc): each of the twelve packs its arguments into a SurfaceControlFrame and + // posts it through RunSurfaceControlFrame - the wire-shaped value channel that replaced the + // function-pointer mailbox (P5c audit row G4). Nine of them map onto the ten wire ops of + // protocol.fbs's SurfaceOp; ServerSwapEGLBuffers, ServerInitCapabilities and + // ServerInitWindowSurface ride the same channel as inproc-only kinds (present travels as a + // record, capabilities are answered by the CapsSnapshot frame, and the third has no caller - + // f0-egl's census, findings F8/§4.2). Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor); Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle); Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height); diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp b/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp new file mode 100644 index 00000000..4bcfd882 --- /dev/null +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp @@ -0,0 +1,52 @@ +// MobileGL - MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "SurfaceControlFrame.h" + +namespace MobileGL::MG_Remote::Server { + + const char* SurfaceControlOpName(SurfaceControlOp op) { + switch (op) { + case SurfaceControlOp::None: return "None"; + case SurfaceControlOp::InitializeDisplay: return "InitializeDisplay"; + case SurfaceControlOp::CreateWindowSurface: return "CreateWindowSurface"; + case SurfaceControlOp::CreatePbufferSurface: return "CreatePbufferSurface"; + case SurfaceControlOp::ResizeWindowSurface: return "ResizeWindowSurface"; + case SurfaceControlOp::ReleaseSurface: return "ReleaseSurface"; + case SurfaceControlOp::MakeCurrent: return "MakeCurrent"; + case SurfaceControlOp::ReleaseCurrent: return "ReleaseCurrent"; + case SurfaceControlOp::SetSwapInterval: return "SetSwapInterval"; + case SurfaceControlOp::ReleaseResources: return "ReleaseResources"; + case SurfaceControlOp::SetWindowHandle: return "SetWindowHandle"; + case SurfaceControlOp::InitCapabilitiesInprocOnly: return "InitCapabilitiesInprocOnly"; + case SurfaceControlOp::SwapBuffersInprocOnly: return "SwapBuffersInprocOnly"; + case SurfaceControlOp::InitWindowSurfaceInprocOnly: return "InitWindowSurfaceInprocOnly"; + case SurfaceControlOp::ProbeForTesting: return "ProbeForTesting"; + } + return ""; + } + + bool SurfaceControlOpHasWireKind(SurfaceControlOp op) { + switch (op) { + case SurfaceControlOp::InitializeDisplay: + case SurfaceControlOp::CreateWindowSurface: + case SurfaceControlOp::CreatePbufferSurface: + case SurfaceControlOp::ResizeWindowSurface: + case SurfaceControlOp::ReleaseSurface: + case SurfaceControlOp::MakeCurrent: + case SurfaceControlOp::ReleaseCurrent: + case SurfaceControlOp::SetSwapInterval: + case SurfaceControlOp::ReleaseResources: + case SurfaceControlOp::SetWindowHandle: + return true; + default: + return false; + } + } + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h new file mode 100644 index 00000000..62145b0a --- /dev/null +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h @@ -0,0 +1,111 @@ +// MobileGL - MobileGL/MG_Remote/Server/SurfaceControlFrame.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P5f, package fc: the EGL/surface control plane, framed. +// +// WHAT THIS REPLACES. The twelve Server* forwarders used to cross to the apply thread through a +// one-slot mailbox carrying a raw FUNCTION POINTER plus a void* to a stack-local Args struct +// (P5c audit row G4). Both halves are meaningless across a process boundary, which is the whole +// reason P5f exists. The frame below is the seam f0-egl's census prescribed: every forwarder's +// arguments converge into one pure-value struct; inproc the struct crosses the (retained, +// blocking, one-slot) channel by value; under spawn the SAME struct is what SurfaceOpCodec +// encodes into a Wire::SurfaceOp. The thread model does not move - only the payload's shape. +// +// THE FRAME CARRIES NO POINTERS, and that is enforced by construction (the static_assert below) +// rather than by review: a field that is a pointer would also be a compile error here. The three +// places the old Args structs held client addresses map onto values: +// +// ServerInitializeEGLDisplay's major/minor out-pointers -> the reply fields eglMajor/eglMinor; +// ServerCreateEGLWindowSurface / ServerSetWindowHandle's const WindowHandle* -> windowBackend + +// nativeToken + width/height (WindowHandle::Handle as an integer); +// every EGL handle (display/surface/context) -> a Uint64 token. Inproc the token IS the handle +// value bit-cast (same process, same driver, and the N-3 tuple bookkeeping compares them); +// under spawn the client mints dense tokens instead (P6-CONTRACT-DRAFT table 0) - the field +// width is already the token's, so nothing here changes shape on that day. +// +// AN ANativeWindow* ON THE WIRE IS REFUSED BY NAME, not carried: nativeToken is only meaningful +// inside the client's process for AndroidNativeWindow, and a spawn-side decode of that kind dies +// with Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (SurfaceOpCodec). Real window arrival +// is P12. + +#pragma once +#include + +#include + +namespace MobileGL::MG_Remote::Server { + + // The operation identity - the one piece of information the retired trampoline function + // pointer carried. The first eleven values are aligned with Wire::SurfaceOpKind + // (protocol.fbs) ON PURPOSE and SurfaceOpCodec pins that agreement with static_asserts; the + // mapping is still an explicit table, never a cast. + enum class SurfaceControlOp : Uint8 { + None = 0, + InitializeDisplay = 1, + CreateWindowSurface = 2, + CreatePbufferSurface = 3, + ResizeWindowSurface = 4, + ReleaseSurface = 5, + MakeCurrent = 6, + ReleaseCurrent = 7, + SetSwapInterval = 8, + ReleaseResources = 9, + SetWindowHandle = 10, + // INPROC-ONLY kinds: legal inside this process's frame channel, NEVER encodable onto the + // wire (SurfaceOpCodec refuses them by name). f0-egl's census found no production caller + // for the two dead forwarders and a CapsSnapshot answer for the third; they ride the same + // frame channel because the function-pointer mailbox is GONE, not because they are wire + // ops. + InitCapabilitiesInprocOnly = 11, // the wire's answer is the CapsSnapshot frame itself + SwapBuffersInprocOnly = 12, // present travels as a record (the class-B Present emitter) + InitWindowSurfaceInprocOnly = 13,// a client-side no-op; kept for the inproc test lane + ProbeForTesting = 14, // MG_Test's arbitrary-work seam through the same channel + }; + + struct SurfaceControlFrame { + SurfaceControlOp kind = SurfaceControlOp::None; + // Minted by the poster (RunSurfaceControlFrame), echoed back with the reply. 0 means + // "never posted". Purely diagnostic inproc; under spawn it is how a SurfaceReply names + // its op. + Uint64 seq = 0; + Uint64 display = 0; + Uint64 surface = 0; // the DRAW surface for MakeCurrent + Uint64 readSurface = 0; // MakeCurrent only; 0 elsewhere + Uint64 context = 0; // MakeCurrent / ReleaseCurrent; 0 elsewhere + // WindowHandle's payload, as values. This is the BACKEND tag (MG_Backend::WindowBackend + // as an Int, -1 = Unknown), not the wire's WindowKind: the apply-side dispatch needs the + // backend enum, and the WindowKind mapping belongs to the wire codec, not to the frame. + Int windowBackend = -1; + Uint64 nativeToken = 0; // HWND / XID / ANativeWindow* as an integer + Int width = 0; + Int height = 0; + Int swapInterval = 0; + // The reply half: written by the dispatch on the apply thread, read by the poster after + // the blocking handshake returns. `ok` is the forwarder's Bool answer; eglMajor/eglMinor + // are InitializeDisplay's out values. + Bool ok = false; + Int eglMajor = 0; + Int eglMinor = 0; + }; + + // THE INVARIANT THE PACKAGE EXISTS FOR. The frame crosses by VALUE - copied into the one-slot + // channel inproc, encoded into a FlatBuffers table under spawn - so it can never carry an + // address: no pointer members, no ownership, trivially copyable, standard layout. + static_assert(std::is_trivially_copyable_v && + std::is_standard_layout_v, + "the surface control frame must stay a plain bag of scalars - a pointer member " + "would be a client address crossing to the server, which is what P5f removes"); + + const char* SurfaceControlOpName(SurfaceControlOp op); + + // True exactly for the ten ops that have a Wire::SurfaceOpKind. The inproc-only kinds + // (InitCapabilities/SwapBuffers/InitWindowSurface, and the test probe) answer false: they may + // ride the inproc frame channel but never the wire. + bool SurfaceControlOpHasWireKind(SurfaceControlOp op); + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index b7257e1d..359c04fe 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -83,11 +83,14 @@ gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS un # StagedTextureStoreTest the texture half of R-11: tx's staged-texture store, its # Fatal{StageSnapshotTooNarrow}, and the E-P5c #2 production wiring # through the real resource op table (P5c tx) +# SurfaceControlFrameTest P5f fc: the value frame that replaced the control mailbox's +# function-pointer payload, its wire codec against protocol.fbs's +# SurfaceOp/SurfaceReply, and the two named refusals at the wire entry # # THIS FILE IS THE PHASE'S ONE RECURRING MERGE CONFLICT, and it is the same-point-append shape # BRIEF §5's ownership table exists to prevent: three packages, three targets, one end-of-file. # The loop is what stops there being a fourth: a package adding a suite adds a NAME. -foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest StagedTextureStoreTest) +foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest StagedTextureStoreTest SurfaceControlFrameTest) add_executable(${wiretest} ${wiretest}.cpp) target_include_directories(${wiretest} PRIVATE diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index d5158b32..f5ae64fa 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -60,7 +60,7 @@ struct FenceBudgetPeer : Codec::WireVerbSink { } Bool OnFenceWaitServer(const MGPFenceWait&) override { AwaitPark(); return true; } void Install() { - if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.SetVerbSink(static_cast(self)); return MOBILEGL_OK; @@ -95,7 +95,7 @@ struct ReadPeer : Codec::WireVerbSink { return true; } void Install() { - auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.SetVerbSink(static_cast(self)); return MOBILEGL_OK; @@ -111,7 +111,7 @@ struct ErrorPeer : Codec::ReplySink { Srv::ServerSessionInstance().Replies().PostReply(seq, status, nullptr, 0); } void Install() { - auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.*PeerMember(RepliesTag{}) = static_cast(self); return MOBILEGL_OK; @@ -307,7 +307,7 @@ TEST(RemoteClientControls, MapPersistentErrorIsNotADecline) { TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) { const auto child = RunInChild([] { StartControlSession(); - if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { auto backend = MakeUnique(); backend->dynamic.MaxComputeWorkGroupCount[0] = 113; Srv::ServerSessionInstance().SetBackend(backend.get()); @@ -335,7 +335,7 @@ TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) { TEST(RemoteClientControls, ADifferentTupleMakeCurrentIsAdoptedWithoutAPumpOrPresent) { const auto child = RunInChild([] { StartControlSession(); - if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { auto backend = MakeUnique(); backend->dynamic.MaxComputeWorkGroupCount[0] = 113; Srv::ServerSessionInstance().SetBackend(backend.get()); @@ -354,7 +354,7 @@ TEST(RemoteClientControls, ADifferentTupleMakeCurrentIsAdoptedWithoutAPumpOrPres GLint before = 0; remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &before); if (before != 113) ::_exit(92); - if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { static_cast(Srv::ServerLoopInstance().Backend())->dynamic.MaxComputeWorkGroupCount[0] = 227; return MOBILEGL_OK; }, nullptr) != MOBILEGL_OK) ::_exit(93); @@ -430,7 +430,7 @@ TEST(RemoteClientControls, ServerRoleRespecifyDoesNotEmitClientInitialBytes) { Srv::ServerSessionInstance().SetBackend(&backend); StartControlSession(); MG_Config::Features.PipePush |= kMGPipeSubsystemResources; - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MG_State::GLState::BufferObject buffer(701); Uint8 data[64]{}; buffer.Respecify(sizeof(data), data); @@ -463,7 +463,7 @@ TEST(RemoteClientControls, ServerRoleFlushDoesNotRunTheClientSubDataFollowup) { Srv::ServerSessionInstance().SetBackend(&backend); StartControlSession(); MG_Config::Features.PipePush |= kMGPipeSubsystemResources; - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MGPipeResourceOps ops{}; ops.Create = [](MGPipeHandle, const MGPResourceDesc&) {}; ops.Respecify = [](MGPipeHandle, const MGPResourceDesc&, const void*) {}; @@ -555,7 +555,7 @@ struct DrawPeer : Codec::WireVerbSink { Bool OnDrawVbo(const MGPDrawInfo&, const MGPDrawRange*, const MGHostSpan*, const MGPDrawIndirect*) override { return true; } void Install() { - Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.SetVerbSink(static_cast(self)); return MOBILEGL_OK; @@ -606,7 +606,7 @@ TEST(RemoteClientControls, ReadPixelsSwapsComponentsInTightAndScatteredReplies) TEST(RemoteClientControls, PersistentMapProducerRefusesTheApplyThread) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { PersistentMapTracker::Instance().PushAllMembers(); return MOBILEGL_OK; }, nullptr); @@ -698,7 +698,7 @@ TEST(RemoteClientControls, ADrawOnASessionWithoutTheRunAheadBitStaysStampedBarri ::_exit(120); // Read the stamp on the thread that owns it: it is a thread_local describing "which // record is the apply thread inside right now", and ApplyOne leaves it standing. - if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { if (Srv::MGPipeServerPublishesRunAhead()) ::_exit(121); MGPDrawInfo probe{}; probe.NumDraws = 1; diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index a87e0df3..33588b1b 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1252,7 +1252,7 @@ struct F1Peer : Codec::WireVerbSink { Bool OnGenerateMipmap(const MGPMipPlan& v) override { mip = v; ++calls; return true; } Bool OnBlit(const MGPBlit& v) override { blit = v; ++calls; return true; } void Install() { - if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.SetVerbSink(static_cast(self)); return MOBILEGL_OK; @@ -1771,7 +1771,7 @@ TEST(RemoteF1, DispatchIndirectStashesTheCommandBuffer) { TEST(RemoteGuards, AllocatorAcquireFromTheApplyThreadIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MGPipeSlots().Acquire(MGPipeKind::Texture, 424242); return MOBILEGL_OK; }, nullptr); @@ -1783,7 +1783,7 @@ TEST(RemoteGuards, AllocatorAcquireFromTheApplyThreadIsFatalByName) { TEST(RemoteGuards, AllocatorFindByLifetimeIdFromTheApplyThreadIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 424242); return MOBILEGL_OK; }, nullptr); @@ -1795,7 +1795,7 @@ TEST(RemoteGuards, AllocatorFindByLifetimeIdFromTheApplyThreadIsFatalByName) { TEST(RemoteGuards, AllocatorFreeFromTheApplyThreadIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MGPipeSlots().Free(MGPipeKind::Texture, {7, 1}); return MOBILEGL_OK; }, nullptr); @@ -1816,7 +1816,7 @@ namespace { TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MG_State::pGLContext = MakeUnique(); TestTextureTable table; MG_State::GLState::TextureObject2D tex(44); @@ -1831,7 +1831,7 @@ TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) { TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { MG_State::pGLContext = MakeUnique(); TestTextureTable table; auto tex = MakeShared(45); @@ -1846,7 +1846,7 @@ TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) { // The four accessor drives share one shape: the BufferObject is constructed on the CLIENT // thread (its own resource_create publication is a legal client-side emit), and only the // accessor runs on the apply thread, where it must die at the accessor's own guard. -#define MGL_BUFFER_GUARD_TEST(Name, Id, Call) TEST(RemoteGuards, Name) { const auto child = RunInChild([] { StartControlSession(); MG_State::GLState::BufferObject buffer(Id); Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { auto& buffer = *static_cast(self); Call; return MOBILEGL_OK; }, &buffer); ClientSessionInstance().Stop(); }); ExpectNamedAbort(child, "Fatal{RoleViolation, \"buffer-legacy-arm\"}"); } +#define MGL_BUFFER_GUARD_TEST(Name, Id, Call) TEST(RemoteGuards, Name) { const auto child = RunInChild([] { StartControlSession(); MG_State::GLState::BufferObject buffer(Id); Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) { auto& buffer = *static_cast(self); Call; return MOBILEGL_OK; }, &buffer); ClientSessionInstance().Stop(); }); ExpectNamedAbort(child, "Fatal{RoleViolation, \"buffer-legacy-arm\"}"); } MGL_BUFFER_GUARD_TEST(BufferMappedDataFromTheApplyThreadIsFatalByName, 703, (void)buffer.MappedData()) MGL_BUFFER_GUARD_TEST(BufferIsMappedFromTheApplyThreadIsFatalByName, 704, (void)buffer.IsMapped()) MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial()) @@ -1881,7 +1881,7 @@ MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 70 TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); return MOBILEGL_OK; @@ -1920,7 +1920,7 @@ TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFat MG_State::GLState::TextureObject2D texture(46); TestTextureTable table; Probe probe{&table, &texture}; - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void* self) -> MobileGLResult { auto& probe = *static_cast(self); // The record being applied is one the client did NOT park behind. The stamp is @@ -1946,7 +1946,7 @@ TEST(RemoteGuards, AnExemptionScopeHeldOnTheGLThreadDoesNotExemptTheApplyThread) const auto child = RunInChild([] { StartControlSession(); const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); return MOBILEGL_OK; @@ -2001,7 +2001,7 @@ TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) { const auto child = RunInChild([] { \ StartControlSession(); \ MG_State::GLState::TextureObject2D texture(Id); \ - Srv::ServerLoopInstance().RunOnApplyThread( \ + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( \ [](void* self) { \ auto& texture = *static_cast(self); \ Call; \ @@ -2155,7 +2155,7 @@ struct PresentCreditPeer : Codec::WireVerbSink { return true; } void Install() { - if (Srv::ServerLoopInstance().RunOnApplyThread( + if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( [](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); decoder.SetVerbSink(static_cast(self)); @@ -2388,7 +2388,7 @@ TEST(RemoteGuards, AResidualPullUnderAnUnbarrieredRecordIsFatalWithoutTheStrictK const auto child = RunInChild([] { MG_Config::Ipc.StrictErrors = false; StartRunAheadSession(); - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear); MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false); @@ -2408,7 +2408,7 @@ TEST(RemoteGuards, AResidualPullUnderABarrieredRecordStillOnlyCounts) { const auto child = RunInChild([] { MG_Config::Ipc.StrictErrors = false; StartRunAheadSession(); - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear); MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); @@ -2430,7 +2430,7 @@ TEST(RemoteGuards, AResidualPullUnderABarrieredRecordStillOnlyCounts) { // the latch and retries, and the burst completes. // // THE DRAIN RUNS ON A THREAD OF ITS OWN, and that is forced by the fixture rather than chosen: -// RunOnApplyThread is synchronous, so the thread that posted the burst cannot also be the +// RunSurfaceControlFrame is synchronous, so the thread that posted the burst cannot also be the // thread that empties the ring. In production it is the GL thread draining at its own waits // (§2.6's deadlock argument); here it is a drainer beside the poster, which puts the producer // in exactly the state that argument describes. @@ -2456,7 +2456,7 @@ TEST(RemoteRunAhead, AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAb // with a drainer racing it - so the full latch is hit many times over rather than // maybe once. That margin is what makes the red-once (restore the Fatal) reliable: // a burst that merely might fill the ring would be a red-once that merely might be red. - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPRange ranges[64]; for (Uint32 r = 0; r < 64; ++r) ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull}; @@ -2484,7 +2484,7 @@ TEST(RemoteGuards, ADeferredDestroyFromAnUnbarrieredApplyIsFatalUnderStrict) { const auto child = RunInChild([] { MG_Config::Ipc.StrictErrors = true; StartRunAheadSession(); - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false); (void)MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture, @@ -2504,7 +2504,7 @@ TEST(RemoteGuards, ADeferredDestroyFromABarrieredApplyIsStillServed) { const auto child = RunInChild([] { MG_Config::Ipc.StrictErrors = true; StartRunAheadSession(); - Srv::ServerLoopInstance().RunOnApplyThread( + Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void*) -> MobileGLResult { MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); if (!MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture, diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 1a1cbfdc..4f8ff1c7 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -288,7 +288,7 @@ TEST(ServerLoopTest, AControlRequestRunsOnTheApplyThreadAndUnparksIt) { std::thread::id posterId{}; std::thread poster([&] { posterId = std::this_thread::get_id(); - rc = loop.RunOnApplyThread( + rc = loop.RunProbeOnApplyThreadForTesting( +[](void* user) -> MobileGLResult { auto* p = static_cast(user); p->ranOn = std::this_thread::get_id(); @@ -375,7 +375,7 @@ TEST(ServerLoopTest, TheControlShadowIsClearedWhenThePumpTakesTheRequest) { std::atomic answered{false}; MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT; std::thread poster([&] { - rc = loop.RunOnApplyThread( + rc = loop.RunProbeOnApplyThreadForTesting( +[](void* user) -> MobileGLResult { Gate* g = static_cast(user); g->running.store(true, std::memory_order_release); @@ -446,10 +446,10 @@ TEST(ServerLoopTest, APostFromTheApplyThreadItselfRunsInlineRatherThanDeadlockin Bool innerRan = false; MobileGLResult innerRc = MOBILEGL_ERR_INVALID_ARGUMENT; } outer; - const MobileGLResult rc = Server::ServerLoopInstance().RunOnApplyThread( + const MobileGLResult rc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void* user) -> MobileGLResult { auto* o = static_cast(user); - o->innerRc = Server::ServerLoopInstance().RunOnApplyThread( + o->innerRc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void* inner) -> MobileGLResult { *static_cast(inner) = true; return MOBILEGL_OK; @@ -905,7 +905,7 @@ TEST(ServerLoopTest, AForwarderCallAfterTheLoopStoppedReturnsNotInitializedAndDo struct Probe { Bool ran = false; } probe; - const MobileGLResult rc = Server::ServerLoopInstance().RunOnApplyThread( + const MobileGLResult rc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void* user) -> MobileGLResult { static_cast(user)->ran = true; return MOBILEGL_OK; @@ -917,6 +917,31 @@ TEST(ServerLoopTest, AForwarderCallAfterTheLoopStoppedReturnsNotInitializedAndDo EXPECT_FALSE(probe.ran) << "the work ran on the caller after the loop stopped"; } +// P5f (fc): the frame channel's own red-once handle. A forwarder no longer posts a function +// pointer and a stack address; it packs a SurfaceControlFrame and the loop's dispatch counter is +// the proof the frame crossed. The revert this guards: turn ServerSetEGLSwapInterval back into a +// direct `Backend()->SetEGLSwapInterval(...)` (or any shape that skips the frame) and the counter +// below does not move - the test goes red even though the swap interval itself would still reach a +// backend in an inproc build, which is exactly why "the EGL call happened" cannot be the check. +TEST(ServerLoopTest, AVoidForwarderCrossesAsOneDispatchedFrame) { + ServerFixture fixture; + ASSERT_TRUE(fixture.Handshake()); + ASSERT_TRUE(fixture.StartLoop()); + Server::ServerLoop& loop = Server::ServerLoopInstance(); + ASSERT_TRUE(fixture.WaitUntilTrulyParked()); + ASSERT_EQ(loop.ControlFramesDispatched(), 0u); + + // No backend lives in this process, so the dispatch declines with NOT_INITIALIZED - but the + // frame must still have CROSSED the channel, which is the property under test. + Server::ServerSetEGLSwapInterval(1); + + EXPECT_EQ(loop.ControlFramesDispatched(), 1u) + << "ServerSetEGLSwapInterval did not dispatch exactly one control frame; the forwarder " + "reached the backend (or did nothing) without crossing the frame channel"; + + fixture.Stop(); +} + // P5d round 3, package D: THE IDENTITY MUST DIE WITH THE THREAD, and this is the case that says // so. OnApplyThread() is no longer `m_running && id == mine` - it is one relaxed load of // Server::Detail::g_applyThreadKey compared against the caller's own thread pointer - so there @@ -928,7 +953,7 @@ TEST(ServerLoopTest, AForwarderCallAfterTheLoopStoppedReturnsNotInitializedAndDo // after a context-loss restart. That thread then answers OnApplyThread() TRUE, which inverts // RunsAsTheServerRole() and PersistentMapTracker::OnServerRole() on the client, aborts // BufferObject's accessors with Fatal{RoleViolation, "buffer-legacy-arm"}, and makes -// RunOnApplyThread run EGL work inline on it - the "EGL on the app thread" outcome R-1 exists to +// RunSurfaceControlFrame run EGL work inline on it - the "EGL on the app thread" outcome R-1 exists to // make impossible. // // Two halves, because the mechanism and the consequence fail differently. (a) is deterministic @@ -1293,10 +1318,10 @@ namespace { } // A control request that runs an arbitrary callable on the apply thread. A std::function is - // fine in a test; production's ControlWork is a raw pointer for the teardown path's sake. + // fine in a test; production's probe hook is a raw pointer for the teardown path's sake. MobileGLResult OnApply(const std::function& body) { std::function copy = body; - return Server::ServerLoopInstance().RunOnApplyThread( + return Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting( +[](void* user) -> MobileGLResult { (*static_cast*>(user))(); return MOBILEGL_OK; @@ -1573,6 +1598,23 @@ TEST(ServerLoopEglTest, ADestroyedContextForgetsTheTupleSoTheSameHandleValuesBin fixture.TearDown(); } +// P5f (fc): InitializeEGLDisplay's out-pointers are the frame's reply fields now (the one place +// the old mailbox carried pointers INTO the poster's stack). The answer is the driver's real +// version through the frame - the BringUp already consumed one init; a second call is an +// idempotent eglInitialize and its version answer must still cross back. +TEST(ServerLoopEglTest, TheInitializeDisplayReplyArrivesThroughTheFrame) { + EglServerFixture fixture; + MGL_EGL_BRING_UP_OR_BAIL(fixture); + + EGLint major = 0; + EGLint minor = 0; + ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor)); + EXPECT_GE(major, 1) << "eglInitialize's major version did not cross back through the frame's " + "reply fields"; + + fixture.TearDown(); +} + // ID-49's tight-size half, gated (review v2 N-5: "nothing, unit and joint"). The client's DstSize is // deliberately WRONG - 80, the size a ROW_LENGTH=8 / SKIP_* client would compute for a 4x3 RGBA8 // read whose tight extent is 48 - and the reply must still be the tight 48 bytes: posted at 48, diff --git a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp new file mode 100644 index 00000000..306e502d --- /dev/null +++ b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp @@ -0,0 +1,362 @@ +// MobileGL - MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P5f, package fc: the control-plane frame and its wire codec. +// +// What is proven here: +// * the frame is a plain bag of scalars (the invariant the package exists for - the old +// mailbox carried a function pointer and the caller's stack address); +// * exactly the ten framed ops have wire kinds, and the two enum mappings (SurfaceControlOp +// <-> ::MobileGL::Wire::SurfaceOpKind, WindowBackend <-> ::MobileGL::Wire::WindowKind) are explicit tables whose +// agreement with the schema is pinned, not assumed; +// * every framed op round-trips through a REAL CtrlEnvelope{SurfaceOp} buffer byte-for-byte; +// * the wire entry refuses by NAME: an ANativeWindow* arriving over the wire dies +// Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (real windows are P12), and a kind the +// schema does not define dies Fatal{ProtocolCorruption, "SurfaceOp"}. +// +// The inproc blocking path itself - posting, parking, the reply half coming back - is gated by +// ServerLoopTest's cases (AVoidForwarderCrossesAsOneDispatchedFrame and the whole +// ServerLoopEglTest fixture, which now drives every forwarder through the frame channel). + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +using namespace MobileGL; + +namespace Server = MobileGL::MG_Remote::Server; +using MobileGL::MG_Remote::DecodeWireSurfaceOp; +using MobileGL::MG_Remote::DecodeWireSurfaceReply; +using MobileGL::MG_Remote::EncodeSurfaceOpFrame; +using MobileGL::MG_Remote::EncodeSurfaceReplyFrame; +using MobileGL::MG_Remote::ServerApplyWireSurfaceOp; +using MobileGL::MG_Remote::SurfaceControlOpForWireKind; +using MobileGL::MG_Remote::SurfaceWireError; +using MobileGL::MG_Remote::WindowBackendForWireWindowKind; +using MobileGL::MG_Remote::WireKindForSurfaceControlOp; +using MobileGL::MG_Remote::WireWindowKindForWindowBackend; +using Server::SurfaceControlFrame; +using Server::SurfaceControlOp; + +namespace { + + std::string g_logPath; + + std::string ReadLog() { + std::ifstream in(g_logPath, std::ios::binary); + if (!in) return {}; + return std::string(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + + unsigned ProcessId() { +#if defined(_WIN32) + return static_cast(_getpid()); +#else + return static_cast(::getpid()); +#endif + } + + // Encode one frame and hand back the parsed wire op. The builder must outlive the parse. + const ::MobileGL::Wire::SurfaceOp* EncodeAndParse(const SurfaceControlFrame& frame, + flatbuffers::FlatBufferBuilder* builder) { + if (EncodeSurfaceOpFrame(frame, builder) != SurfaceWireError::None) return nullptr; + const auto* envelope = ::MobileGL::Wire::GetCtrlEnvelope(builder->GetBufferPointer()); + if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::SurfaceOp) return nullptr; + return envelope->msg_as_SurfaceOp(); + } + +} // namespace + +// The invariant the package exists for, in executable form: the channel's payload can hold no +// address. The compile-time twin is the static_assert in SurfaceControlFrame.h; this case is the +// readable one, and it is what a future "just add a pointer field" edit trips over FIRST (the +// static_assert) - the test exists so the failure also has a name in the ctest log. +TEST(SurfaceControlFrameTest, TheFrameIsAPlainBagOfScalars) { + EXPECT_TRUE(std::is_trivially_copyable_v) + << "the frame grew a non-trivial member; the one-slot channel copies it by value"; + EXPECT_TRUE(std::is_standard_layout_v) + << "the frame lost standard layout; SurfaceOpCodec encodes it field by field and the two " + "shapes must stay obvious to a reader"; +} + +TEST(SurfaceControlFrameTest, ExactlyTheTenFramedOpsHaveWireKindsAndTheValuesArePinned) { + // The ten framed ops, in schema order. The numeric agreement with ::MobileGL::Wire::SurfaceOpKind is + // asserted per op rather than trusted from the static_asserts, because THIS is the table a + // reader consults when the schema next changes. + const SurfaceControlOp wired[] = { + SurfaceControlOp::InitializeDisplay, SurfaceControlOp::CreateWindowSurface, + SurfaceControlOp::CreatePbufferSurface, SurfaceControlOp::ResizeWindowSurface, + SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent, + SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval, + SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle, + }; + for (const SurfaceControlOp op : wired) { + ::MobileGL::Wire::SurfaceOpKind kind; + ASSERT_TRUE(WireKindForSurfaceControlOp(op, &kind)) + << Server::SurfaceControlOpName(op) << " lost its wire kind"; + EXPECT_EQ(static_cast(kind), static_cast(op)) + << Server::SurfaceControlOpName(op) << ": the frame and wire enums drifted apart"; + SurfaceControlOp back; + ASSERT_TRUE(SurfaceControlOpForWireKind(kind, &back)); + EXPECT_EQ(back, op); + } + // fc's three schema additions are APPEND-ONLY: these values are wire ABI, not taste. + EXPECT_EQ(static_cast(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval), 8u); + EXPECT_EQ(static_cast(::MobileGL::Wire::SurfaceOpKind::ReleaseResources), 9u); + EXPECT_EQ(static_cast(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle), 10u); + + // The inproc-only kinds refuse the wire by name, both directions. + const SurfaceControlOp inprocOnly[] = { + SurfaceControlOp::None, SurfaceControlOp::InitCapabilitiesInprocOnly, + SurfaceControlOp::SwapBuffersInprocOnly, SurfaceControlOp::InitWindowSurfaceInprocOnly, + SurfaceControlOp::ProbeForTesting, + }; + for (const SurfaceControlOp op : inprocOnly) { + ::MobileGL::Wire::SurfaceOpKind kind; + EXPECT_FALSE(WireKindForSurfaceControlOp(op, &kind)) + << Server::SurfaceControlOpName(op) << " may never leave the process"; + } + SurfaceControlOp op; + EXPECT_FALSE(SurfaceControlOpForWireKind(::MobileGL::Wire::SurfaceOpKind::None, &op)); + EXPECT_FALSE(SurfaceControlOpForWireKind(static_cast<::MobileGL::Wire::SurfaceOpKind>(200), &op)) + << "a wire kind the schema does not define must not decode into an op"; +} + +TEST(SurfaceControlFrameTest, WindowBackendAndWireWindowKindMapExplicitlyBothWays) { + using MG_Backend::WindowBackend; + const struct { + WindowBackend backend; + ::MobileGL::Wire::WindowKind kind; + } table[] = { + {WindowBackend::Android, ::MobileGL::Wire::WindowKind::AndroidNativeWindow}, + {WindowBackend::X11, ::MobileGL::Wire::WindowKind::X11}, + {WindowBackend::MetalLayer, ::MobileGL::Wire::WindowKind::MetalLayer}, + {WindowBackend::Win32, ::MobileGL::Wire::WindowKind::Win32Hwnd}, + {WindowBackend::Unknown, ::MobileGL::Wire::WindowKind::None}, + }; + for (const auto& row : table) { + ::MobileGL::Wire::WindowKind kind; + ASSERT_TRUE(WireWindowKindForWindowBackend(row.backend, &kind)); + EXPECT_EQ(kind, row.kind); + WindowBackend backend; + ASSERT_TRUE(WindowBackendForWireWindowKind(row.kind, &backend)); + EXPECT_EQ(backend, row.backend); + } + // fc added MetalLayer to the schema; the value is wire ABI. + EXPECT_EQ(static_cast(::MobileGL::Wire::WindowKind::MetalLayer), 6u); + + // Surfaceless and Pbuffer are surface SHAPES, not window backends: no answer, by name. + WindowBackend backend; + EXPECT_FALSE(WindowBackendForWireWindowKind(::MobileGL::Wire::WindowKind::Surfaceless, &backend)); + EXPECT_FALSE(WindowBackendForWireWindowKind(::MobileGL::Wire::WindowKind::Pbuffer, &backend)); + EXPECT_FALSE(WindowBackendForWireWindowKind(static_cast<::MobileGL::Wire::WindowKind>(200), &backend)); + + // And the two enums are NOT integer-compatible, which is the whole reason the mapping is a + // table: a cast would shift every answer (Android is 0 in one and 1 in the other). + EXPECT_NE(static_cast(WindowBackend::Android), + static_cast(::MobileGL::Wire::WindowKind::AndroidNativeWindow)) + << "the enums became integer-compatible; a future cast-based shortcut would silently " + "pass this test - keep it red-shaped"; +} + +TEST(SurfaceControlFrameTest, EveryFramedOpRoundTripsThroughTheWireEnvelope) { + const SurfaceControlOp wired[] = { + SurfaceControlOp::InitializeDisplay, SurfaceControlOp::CreateWindowSurface, + SurfaceControlOp::CreatePbufferSurface, SurfaceControlOp::ResizeWindowSurface, + SurfaceControlOp::ReleaseSurface, SurfaceControlOp::MakeCurrent, + SurfaceControlOp::ReleaseCurrent, SurfaceControlOp::SetSwapInterval, + SurfaceControlOp::ReleaseResources, SurfaceControlOp::SetWindowHandle, + }; + for (const SurfaceControlOp op : wired) { + SurfaceControlFrame frame; + frame.kind = op; + frame.seq = 0x1122334455667788ull; + frame.display = 11; + frame.surface = 22; + frame.readSurface = 33; + frame.context = 44; + frame.width = 1920; + frame.height = 1080; + frame.swapInterval = 3; + if (op == SurfaceControlOp::CreateWindowSurface || op == SurfaceControlOp::SetWindowHandle) { + frame.windowBackend = static_cast(MG_Backend::WindowBackend::X11); + frame.nativeToken = 0xDEADBEEF; + } + flatbuffers::FlatBufferBuilder builder(256); + const ::MobileGL::Wire::SurfaceOp* wireOp = EncodeAndParse(frame, &builder); + ASSERT_NE(wireOp, nullptr) << Server::SurfaceControlOpName(op) << " failed to encode"; + + SurfaceControlFrame back; + ASSERT_EQ(DecodeWireSurfaceOp(*wireOp, &back), SurfaceWireError::None) + << Server::SurfaceControlOpName(op); + EXPECT_EQ(back.kind, frame.kind); + EXPECT_EQ(back.seq, frame.seq); + EXPECT_EQ(back.display, frame.display); + EXPECT_EQ(back.surface, frame.surface); + EXPECT_EQ(back.readSurface, frame.readSurface) << "readSurface must round-trip (the " + "MakeCurrent four-tuple)"; + EXPECT_EQ(back.context, frame.context); + EXPECT_EQ(back.width, frame.width); + EXPECT_EQ(back.height, frame.height); + EXPECT_EQ(back.swapInterval, frame.swapInterval); + EXPECT_EQ(back.windowBackend, frame.windowBackend) + << Server::SurfaceControlOpName(op); + EXPECT_EQ(back.nativeToken, frame.nativeToken); + } +} + +TEST(SurfaceControlFrameTest, TheReplyHalfRoundTripsAsASurfaceReply) { + SurfaceControlFrame frame; + frame.kind = SurfaceControlOp::InitializeDisplay; + frame.seq = 4242; + frame.ok = true; + frame.eglMajor = 1; + frame.eglMinor = 5; + + flatbuffers::FlatBufferBuilder builder(256); + EncodeSurfaceReplyFrame(frame, &builder); + const auto* envelope = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer()); + ASSERT_NE(envelope, nullptr); + ASSERT_EQ(envelope->msg_type(), ::MobileGL::Wire::CtrlMsg::SurfaceReply); + const ::MobileGL::Wire::SurfaceReply* reply = envelope->msg_as_SurfaceReply(); + ASSERT_NE(reply, nullptr); + + SurfaceControlFrame back; + DecodeWireSurfaceReply(*reply, &back); + EXPECT_EQ(back.seq, 4242ull) << "a reply must name its op"; + EXPECT_TRUE(back.ok); + EXPECT_EQ(back.eglMajor, 1); + EXPECT_EQ(back.eglMinor, 5); +} + +TEST(SurfaceControlFrameTest, MalformedAndInprocOnlyFramesRefuseToEncode) { + flatbuffers::FlatBufferBuilder builder(256); + SurfaceControlFrame frame; // kind == None + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::UnknownOpKind); + + frame.kind = SurfaceControlOp::ProbeForTesting; + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::InprocOnlyOpOnTheWire); + frame.kind = SurfaceControlOp::SwapBuffersInprocOnly; + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::InprocOnlyOpOnTheWire); + frame.kind = SurfaceControlOp::InitCapabilitiesInprocOnly; + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::InprocOnlyOpOnTheWire); + frame.kind = SurfaceControlOp::InitWindowSurfaceInprocOnly; + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::InprocOnlyOpOnTheWire); + + // A window op whose backend tag names no WindowBackend. + frame.kind = SurfaceControlOp::CreateWindowSurface; + frame.windowBackend = 99; + EXPECT_EQ(EncodeSurfaceOpFrame(frame, &builder), SurfaceWireError::UnknownWindowKind); +} + +TEST(SurfaceControlFrameTest, AWireWindowKindThatNamesNoBackendFailsDecodeByName) { + flatbuffers::FlatBufferBuilder builder(256); + const auto op = ::MobileGL::Wire::CreateSurfaceOp( + builder, 1, ::MobileGL::Wire::SurfaceOpKind::SetWindowHandle, 0, 0, ::MobileGL::Wire::WindowKind::Surfaceless, 0, 0, + 0, 0, 0, 0); + const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + const ::MobileGL::Wire::SurfaceOp* wireOp = + ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp(); + ASSERT_NE(wireOp, nullptr); + + SurfaceControlFrame frame; + EXPECT_EQ(DecodeWireSurfaceOp(*wireOp, &frame), SurfaceWireError::WindowKindNamesNoBackend); + + // And the Android refusal is a DECODE answer before it is a Fatal: the pure half. + flatbuffers::FlatBufferBuilder androidBuilder(256); + const auto androidOp = ::MobileGL::Wire::CreateSurfaceOp( + androidBuilder, 1, ::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface, 0, 0, + ::MobileGL::Wire::WindowKind::AndroidNativeWindow, 0x1234, 800, 600, 0, 0, 0); + const auto androidEnvelope = + ::MobileGL::Wire::CreateCtrlEnvelope(androidBuilder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, androidOp.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(androidBuilder, androidEnvelope); + const ::MobileGL::Wire::SurfaceOp* androidWireOp = + ::MobileGL::Wire::GetCtrlEnvelope(androidBuilder.GetBufferPointer())->msg_as_SurfaceOp(); + ASSERT_NE(androidWireOp, nullptr); + EXPECT_EQ(DecodeWireSurfaceOp(*androidWireOp, &frame), + SurfaceWireError::AndroidNativeWindowArrived); +} + +#if !defined(_WIN32) +// The refusal, as a death. An ANativeWindow* is a pointer into the CLIENT's process; on the +// wire it can only mean "a real window tried to cross", and that is P12's story, not a surface +// the P5f/P6 server can create. The child dies by name, and the log line is asserted - a death +// test that only checks for a crash goes green on any other abort in the same body. +TEST(SurfaceControlFrameTest, AnAndroidNativeWindowOnTheWireIsRefusedByName) { + flatbuffers::FlatBufferBuilder builder(256); + const auto op = ::MobileGL::Wire::CreateSurfaceOp( + builder, 9, ::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface, 1, 2, + ::MobileGL::Wire::WindowKind::AndroidNativeWindow, 0x1234, 800, 600, 0, 0, 0); + const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + const ::MobileGL::Wire::SurfaceOp* wireOp = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp(); + ASSERT_NE(wireOp, nullptr); + + EXPECT_EXIT(ServerApplyWireSurfaceOp(*wireOp, nullptr), ::testing::KilledBySignal(SIGABRT), ".*"); + const std::string log = ReadLog(); + EXPECT_NE(log.find("Fatal{UnmigratedSurface, \"AndroidNativeWindow@P12\"}"), std::string::npos) + << "the abort happened but not for this rule's reason; the log says: " << log; +} + +// A kind the schema does not define (a NEWER client's op reaching an OLDER server, or +// corruption) is Fatal{ProtocolCorruption}, never ignored: an ignored control op is a client +// waiting on a reply that never comes, which is a hang wearing a green lane. +TEST(SurfaceControlFrameTest, AnUnknownWireOpKindIsProtocolCorruptionByName) { + flatbuffers::FlatBufferBuilder builder(256); + const auto op = ::MobileGL::Wire::CreateSurfaceOp(builder, 9, static_cast<::MobileGL::Wire::SurfaceOpKind>(200), 1, 2, + ::MobileGL::Wire::WindowKind::None, 0, 0, 0, 0, 0, 0); + const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + const ::MobileGL::Wire::SurfaceOp* wireOp = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp(); + ASSERT_NE(wireOp, nullptr); + + EXPECT_EXIT(ServerApplyWireSurfaceOp(*wireOp, nullptr), ::testing::KilledBySignal(SIGABRT), ".*"); + const std::string log = ReadLog(); + EXPECT_NE(log.find("Fatal{ProtocolCorruption, \"SurfaceOp\"}"), std::string::npos) + << "the abort happened but not for this rule's reason; the log says: " << log; +} +#endif + +int main(int argc, char** argv) { + // Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first + // write, and caches the FILE*. The name carries this process's pid, because + // gtest_discover_tests runs every case as its own process, in parallel under ctest -j. + // (ServerLoopTest's main, verbatim - the Fatal arms report through MGLOG_F, which writes to + // a named file and never to stderr.) + namespace fs = std::filesystem; + const fs::path path = + fs::temp_directory_path() / ("mobilegl-surfaceframe-test-" + std::to_string(ProcessId()) + ".log"); + std::error_code ec; + fs::remove(path, ec); + g_logPath = path.string(); +#if defined(_WIN32) + _putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str()); +#else + setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); +#endif + ::testing::InitGoogleTest(&argc, argv); + const int rc = RUN_ALL_TESTS(); + fs::remove(path, ec); + return rc; +} diff --git a/docs/Disaggregated/P5F-WIRE-COMPLETENESS.md b/docs/Disaggregated/P5F-WIRE-COMPLETENESS.md index 358b4bcb..41cf43ed 100644 --- a/docs/Disaggregated/P5F-WIRE-COMPLETENESS.md +++ b/docs/Disaggregated/P5F-WIRE-COMPLETENESS.md @@ -3,7 +3,9 @@ > 基线 `feat/disaggregated @ 7f1d2134`。路径在 `MobileGL/` 下。 > **进度**:`f0` 普查已落(`notes/p5f/` 八篇);`f1` 双块机制已落(报告 > [`notes/p5f/f1-report.md`](notes/p5f/f1-report.md):旋钮 `MOBILEGL_IPC_ROLE_SPLIT_STATE`、 -> 车道标签 `integration-dualblock-split`、棘轮 `Harness/dualblock-expected-fatals.txt`)。 +> 车道标签 `integration-dualblock-split`、棘轮 `Harness/dualblock-expected-fatals.txt`); +> `fc` 控制面帧已落(报告 [`notes/p5f/fc-report.md`](notes/p5f/fc-report.md):§2.3 的函数指针邮箱 +> 退役为 `SurfaceControlFrame` 值帧,schema 缺口补齐,wire 入口具名拒绝就位)。 > **P6 在 P5f 之前不开工**:P6 的前提"只是传输替换"今天不成立,§2 是不成立的清单。 --- @@ -188,6 +190,10 @@ Magma 今天不发布 `kCapRunAheadApply`,所以它跑 lockstep——**lockste `SurfaceOp` / `SurfaceReply` / `WindowKind` schema 已定但**无人使用**。 跨进程后函数指针与栈地址都无意义。 +> **fc 已落地**:行号以基线 `7f1d2134` 为准(本节是"在基线头上实测"的清单);落地后的形态、 +> 逐 op 映射与契约草稿见 [`notes/p5f/fc-report.md`](notes/p5f/fc-report.md) 与 +> [`notes/p5f/fc-contract-draft.md`](notes/p5f/fc-contract-draft.md)。 + ### 2.4 进程级静态,语义却是 per-context 已知两个:`g_syncedRenderStateParameters`(`MG_Backend/DirectGLES/DirectGLES.cpp:4586`)与 diff --git a/docs/Disaggregated/P6-CONTRACT-DRAFT.md b/docs/Disaggregated/P6-CONTRACT-DRAFT.md index 967cd035..e08d800d 100644 --- a/docs/Disaggregated/P6-CONTRACT-DRAFT.md +++ b/docs/Disaggregated/P6-CONTRACT-DRAFT.md @@ -39,7 +39,7 @@ other gate here. | row | encoding | invalid | consumer | |---|---|---|---| | `DisplayToken` / `SurfaceToken` / `ContextToken` | `Uint64`, minted by the client, dense from 1, never reused in a session. Server keeps token → native handle; it never sees the client's EGL values. `protocol.fbs:186-196`'s `display` / `surface` are already `ulong` and are these. | `0` on any op but `ReleaseCurrent` → `Fatal{ProtocolCorruption}` | the server's surface map | -| `SurfaceOpKind` additions | Append-only to `protocol.fbs:166-175`. Expected `SetSwapInterval`, `ReleaseResources`, `SetWindowHandle` — **`PENDING a6`** (audit item 4). | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | `ServerMain`'s control pump | +| `SurfaceOpKind` additions | **LANDED by P5f `fc`** (append-only, `protocol.fbs:166-179`): `SetSwapInterval=8`, `ReleaseResources=9`, `SetWindowHandle=10`, plus `WindowKind::MetalLayer=6` and `SurfaceOp.readSurface/context` (f0-egl's schema gap list, confirmed to the letter). | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | `ServerMain`'s control pump | | `WindowKind::AndroidNativeWindow` at the server | Refusal: `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`. | — | §4.3 | | `FatalCode::ServerCrashed` / `DeviceLost` | Already allocated (`protocol.fbs:234-242`); P6 is the first phase that can produce either. | — | §5 | | ABI fingerprint | Unchanged. `CapsCodec.h:23-29`: spawn is same-machine, same-binary, so the existing `sizeof(DynamicBackendParameters)` / `sizeof(MGPCaps)` / build-fingerprint handshake is inherited. Fixed-width rewrite stays P7's. | mismatch → `Fatal{AbiMismatch}` | `SessionHandshakeTest.cpp:115` | @@ -80,20 +80,30 @@ other gate here. ## §4 The control plane (`cp`) -1. `Server/ServerLoop.cpp:802-1022`'s twelve `Server*` forwarders each post a function pointer plus - a stack-local `void*` to a one-slot mailbox (`ServerLoop.cpp:533-568`, `:645`) — P5c audit row - G4, recorded as P6's (`CONTRACT-P5C.md:537`). Neither has meaning across a process, so each - becomes a `SurfaceOp` / `SurfaceReply` pair. -2. Reconciliation against the seven existing enumerators: `ServerSwapEGLBuffers` stays a record with - its own present credit (P5e ruling, `Client/BackendObject_Remote.cpp`); `ServerInitCapabilities` - is answered by `CapsSnapshot`; `ServerInitWindowSurface` is a client-side no-op. The remaining - nine map onto the seven plus §1's additions. **`PENDING a6`**. +1. **The framing itself LANDED in P5f, package `fc`** (`docs/Disaggregated/notes/p5f/fc-report.md`). + The twelve `Server*` forwarders no longer post a function pointer plus a stack-local `void*` to a + one-slot mailbox (that was P5c audit row G4, recorded as P6's by `CONTRACT-P5C.md:537`): each + packs a value-only `Server::SurfaceControlFrame` + (`MG_Remote/Server/SurfaceControlFrame.h`) and the one blocking slot carries it by value. P6's + remainder is TRANSPORT ONLY: the client encodes the frame with + `MG_Remote::EncodeSurfaceOpFrame` (`MG_Remote/Protocol/SurfaceOpCodec.h`) onto the control + socket, the server's pump decodes and enters through + `MG_Remote::ServerApplyWireSurfaceOp`, and the reply crosses as `SurfaceReply`. +2. Reconciliation against the enumerators (f0-egl §4.2, landed): `ServerSwapEGLBuffers` stays a + record with its own present credit (P5e ruling, `Client/BackendObject_Remote.cpp`); + `ServerInitCapabilities` is answered by `CapsSnapshot`; `ServerInitWindowSurface` is a + client-side no-op. The three ride the inproc frame channel as inproc-only kinds that the codec + refuses to encode (`SurfaceWireError::InprocOnlyOpOnTheWire`). The remaining nine map onto the + seven plus §1's additions - ten wire ops with `ReleaseCurrent`. 3. **Real windows are refused, by name.** `WindowHandle::Handle` (`MG_Backend/BackendObject.h:561-566`) - is an `ANativeWindow*` on Android. P6 lands pbuffer / surfaceless / offscreen — what `HeadlessGL`, - the split-equivalent lane and trace replay need, and what P6's exit gate measures. **Real window - arrival is P12.** -4. `ForgetCurrentTuple`'s N-3 rule (`ServerLoop.cpp:318-330`) is a backend property, not a transport - one, and must hold identically on the frame path. + is an `ANativeWindow*` on Android. fc landed the refusal at the wire entry: a decoded + `WindowKind::AndroidNativeWindow` dies `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}` + (`SurfaceOpCodec.cpp` `ServerApplyWireSurfaceOp`). P6 lands pbuffer / surfaceless / offscreen — + what `HeadlessGL`, the split-equivalent lane and trace replay need, and what P6's exit gate + measures. **Real window arrival is P12.** +4. `ForgetCurrentTuple`'s N-3 rule holds identically on the frame path: the forgetting calls moved + INTO the dispatch (`ServerLoop.cpp` `ApplySurfaceControlFrame`), and ServerLoopTest's C7/N-3 + controls drive them through framed posts unchanged. ## §5 Death, and what P5e changed diff --git a/docs/Disaggregated/P6-SPAWN-PLAN.md b/docs/Disaggregated/P6-SPAWN-PLAN.md index 45d7f561..504bb98f 100644 --- a/docs/Disaggregated/P6-SPAWN-PLAN.md +++ b/docs/Disaggregated/P6-SPAWN-PLAN.md @@ -29,7 +29,7 @@ 1. `SocketTransport`。`ClientSession.cpp:468` 目前硬拒非 `InProcess`。 2. `ServerMain`。今天的 `MobileGLServer`(`CMakeLists.txt:923`)是 P0 spike 桩(`tools/spikes/server_stub/main.cpp`),Android-only、默认 OFF。 3. 进程机制:fork/execve;envp 剔除 + 子进程强制 monolith(双保险);`dladdr` 兜底;有界重试握手;EOF 即退;不留孤儿。 -4. 控制面帧。十二个 `Server*` forwarder(`ServerLoop.cpp:802-1022`)经单槽邮箱传**函数指针 + 栈上 `void*`**(`ServerLoop.cpp:533-568`、`:645`),跨进程无意义。P5c 审计 G4 行,记给 P6。 +4. 控制面帧。**P5f 包 fc 已把帧化本身落地**:forwarder 改发 `SurfaceControlFrame` 值帧(`MG_Remote/Server/SurfaceControlFrame.h`),schema 缺口(3 枚枚举 + `readSurface`/`context` 字段 + `WindowKind::MetalLayer`)已按 append-only 补齐,wire 编解码与具名拒绝在 `MG_Remote/Protocol/SurfaceOpCodec.cpp`(`ServerApplyWireSurfaceOp`)。P6 的 `cp` 只剩传输:client 侧 `EncodeSurfaceOpFrame` 上 socket、server 泵进 `ServerApplyWireSurfaceOp`、`SurfaceReply` 回程与 §4 的死亡接线。P5c 审计 G4 行,原记 P6。 5. `g_syncedRenderStateParameters`(`MG_Backend/DirectGLES/DirectGLES.cpp:4586`)与 `ScopedDefaultUnpackState::s_synced` 按 context 世代重置(`CONTRACT-P5C.md:538-539`)。 6. 死亡语义,见 §4。 @@ -80,7 +80,7 @@ a6 (只读审计,不写代码) **so**:`SocketTransport`。**必须在第二个进程出现之前就绿**——一次 `socketpair()` + 两个线程跑 `InProcessTransportTest` 全套,含 `BUFFER_TOO_SMALL` 留帧语义。理由同 `SessionRings.h:22-28`。 -**sm / cp / st / t6**:见 §2 与 §3。`cp` 最大;`ForgetCurrentTuple` 的 N-3 语义(`ServerLoop.cpp:318-330`)须在帧路径上原样成立。 +**sm / cp / st / t6**:见 §2 与 §3。`cp` 在 fc 之后只剩传输;`ForgetCurrentTuple` 的 N-3 语义已在帧路径的分发里原样成立(`ApplySurfaceControlFrame`,ServerLoopTest 的 C7/N-3 对照驱动)。 ## 6 出口门 diff --git a/docs/Disaggregated/ROADMAP.md b/docs/Disaggregated/ROADMAP.md index bf2d2c12..2d8e8e88 100644 --- a/docs/Disaggregated/ROADMAP.md +++ b/docs/Disaggregated/ROADMAP.md @@ -68,7 +68,7 @@ CTS 周转单独计价(`gl44to46` 约 56,271 例):逐阶段只跑该阶段 | G1 | shared | `gPipeInputs`:client 残余填充写、applier 写、server 的 36 行 BARRIER-PULLED 读 | `MobileGL/MG_Backend/MGPipe/PipeInputs.h:806`;填充 `MobileGL/MG_Impl/Pipe/PipeFill.cpp:2798`;applier `MobileGL/MG_Pipe/PipeApply.cpp:1415-1560`;stamp `MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:103-132`;`rsp` `MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:72` | **只靠 verb barrier** 保证单写者 | `rsp` + strict Fatal + poison | 值类行(`GetActiveTextureUnit`、`GetMaxTouchedTextureUnit`、`GetTouchedBufferBindingPointCount`、`GetCurrentVertexAttribute`、XFB 六项、三个世代)→ 每 verb 一条残余值记录或按 `Coverage.def` 由 server 自答;**对象类行**(`GetBoundVertexArray`、`GetProgramForDraw/Dispatch`、`GetTextureUnitObject`、`GetImageTextureBinding`、`GetFramebufferBindingSlot`、`GetBufferBindingSlot/Point`、sticky `GetTextureObject` / `GetProgramObject`)返回前端对象指针、无法序列化,留 P3b/P4b/P7/P8 的 twin 表 | | G2 | C→S 写 | GL 线程在 `FreshlyPrimed` 时整体 `MGPipeApplierReset()` server 的 `g_applier` | `MobileGL/MG_Impl/Pipe/PipeFill.cpp:2654-2656` → `MobileGL/MG_Pipe/PipeApply.cpp:1202-1299` | barrier 的后置条件(validate 时无在飞记录) | 无 | make-current 边上的 `applier_reset` 控制记录 | | G3 | C→S 写 | 六种前端对象死亡:栈上结构体指针经 mailbox 送到 apply 线程(framebuffer 没有 wire delete opcode) | `DirectGLES/Managers.cpp:206-228` | 阻塞 + 同地址空间 | 无 | `object_death` wire 记录 | -| G4 | C→S 控制 | 十二个 `Server*` EGL forwarder 经 one-slot mailbox 传**函数指针 + `void*` 栈局部** | `MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:125-264`;`MobileGL/MG_Remote/Server/ServerLoop.cpp:543-598` | 阻塞 | `Fatal{ApplyThreadNotRunning}` | P6 的控制面帧(P5c 不做) | +| G4 | C→S 控制 | 十二个 `Server*` EGL forwarder 经 one-slot mailbox 传**函数指针 + `void*` 栈局部** | `MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:125-264`;邮箱本体在 fc 前的 HEAD 上是 `ServerLoop.cpp:533-568`(PumpControlRequest)+ `:645-707`(RunOnApplyThread)——本行原引 `:543-598`,f0-egl F9 修正 | 阻塞 | `Fatal{ApplyThreadNotRunning}` | **已由 P5f 包 fc 帧化**(原记 P6):`SurfaceControlFrame` 纯值帧走同一阻塞通道(`Server/SurfaceControlFrame.h`、分发 `ApplySurfaceControlFrame`),wire 编解码与具名拒绝在 `Protocol/SurfaceOpCodec.cpp`;报告 `notes/p5f/fc-report.md` | | G5 | C→S 读 | `ServerLoop::OnApplyThread()` 每条 routed 记录读;bring-up 直接读 `loop.Backend()` / `MGPipeGetResourceOps()` | `MobileGL/MG_Remote/Client/WireTables.cpp:71`;`MobileGL/MG_Backend/Init.cpp:100`、`:145`、`:164-170`、`:247` | 同进程 | — | client 本地角色标志;caps 快照的 cap 位(P6 顺手) | | G6 | shared | 前端地址 / lifetime-id 键控的 Espryt twin registry;持前端裸指针的 memo(`ResolvedDrawBuffers::Entry::frontend`、三张 texture sync list);server 读 `pDefaultFramebufferInfo`;`g_rawDepthFetchSamplerState`;`ScopedDefaultUnpackState::s_synced` 从不按 context 世代重置 | `DirectGLES/Managers.h:309-377`、`:1185`;`DirectGLES/DirectGLES.cpp:61-62`、`:1958-1987`、`:2846`;`DirectGLES/Managers.cpp:5806-5812` | barrier / server 私有 | — | P3b/P4b 的 twin 表重键与 D-C1;`s_synced` 按 context 世代重置归 P6 | | A1 | — | barrier 的 client 半边断言从未接线:`InBarrierWait()` 零调用点,只有 `ApplyThreadIsInsideApplier` 在 emit 时检查 | `MobileGL/MG_Remote/Client/ClientSession.cpp:880`、`:748-753` | — | — | 接上,或删掉契约里的这句声明 | @@ -100,7 +100,7 @@ CTS 周转单独计价(`gl44to46` 约 56,271 例):逐阶段只跑该阶段 ### 留给后续阶段的(P5c 不碰) -对象类 BARRIER-PULLED 行与 twin registry 的前端键(P3b/P4b、P7);XFB scatter / `OnXfbScatterReady`、`OnTexturePullRequest` 与终止符、`OnGlError` 有序化(P9);EGL forwarder 的控制面(P6);`copy-image-shadow-mirror` 与 CopyTex 纹素回写(P8 / P9);`s_synced` / `g_syncedRenderStateParameters` 的 context 世代重置(P6)。 +对象类 BARRIER-PULLED 行与 twin registry 的前端键(P3b/P4b、P7);XFB scatter / `OnXfbScatterReady`、`OnTexturePullRequest` 与终止符、`OnGlError` 有序化(P9);EGL forwarder 的控制面(原记 P6,**P5f 已收回为包 fc 并落地**,见 G4 行);`copy-image-shadow-mirror` 与 CopyTex 纹素回写(P8 / P9);`s_synced` / `g_syncedRenderStateParameters` 的 context 世代重置(P6)。 ## 里程碑 diff --git a/docs/Disaggregated/notes/p5f/fc-contract-draft.md b/docs/Disaggregated/notes/p5f/fc-contract-draft.md new file mode 100644 index 00000000..c67ac89f --- /dev/null +++ b/docs/Disaggregated/notes/p5f/fc-contract-draft.md @@ -0,0 +1,95 @@ +# fc — 控制面帧契约草稿(P5f 包 fc;CONTRACT-P5F.md 本体留给集成收口) + +> Status: DRAFT, landed in code by package `fc` on branch `p5f/fc`. Authority once integrated: this +> feeds CONTRACT-P5F.md. Until then it supplements CONTRACT-P5C (audit row G4, discharged here) and +> CONTRACT-P5E §2.5 (the wait-before-forwarder discipline, unchanged), and it answers the +> control-plane rows of `docs/Disaggregated/P6-CONTRACT-DRAFT.md` §4 ahead of schedule. +> Code anchors: `MobileGL/MG_Remote/Server/SurfaceControlFrame.h` (the frame), +> `MobileGL/MG_Remote/Protocol/SurfaceOpCodec.{h,cpp}` (the wire codec and the named refusals), +> `MobileGL/MG_Remote/Server/ServerLoop.cpp` (`RunSurfaceControlFrame` / +> `ApplySurfaceControlFrame`). + +--- + +## 1. Ownership + +1. A **SurfaceControlFrame is a value**. The poster fills the request fields; ownership of the + slot's content passes to the channel at publish; the dispatch on the apply thread fills the + reply half (`ok`, `eglMajor`, `eglMinor`) into the same value; the poster's copy-out happens + after the blocking handshake returns. No field is a pointer and none may become one — the + `static_assert(std::is_trivially_copyable_v<...>)` in `SurfaceControlFrame.h` is the + enforcement, not a courtesy. +2. `seq` is minted by the channel's poster side (`RunSurfaceControlFrame`), monotonically from 1 + per session, and echoed in the reply. Under spawn the client mints (P6-CONTRACT-DRAFT table 0); + the field does not change shape. +3. `display` / `surface` / `readSurface` / `context` are Uint64 **tokens**. Inproc they are the + client's EGL handle values bit-cast — legal only because both roles share a process and a + driver, and the N-3 tuple bookkeeping compares them by value. Under spawn they are + client-minted dense tokens (P6-CONTRACT-DRAFT table 0, unchanged). No code may dereference a + token as a pointer on a path a non-inproc transport can reach. +4. `windowBackend` + `nativeToken` + `width`/`height` carry a `WindowHandle` as values. The frame + names the **backend** enum, never the wire's `WindowKind`; the two enums meet only inside + `SurfaceOpCodec.cpp`'s explicit tables (`Surfaceless`/`Pbuffer` are surface shapes and answer + "no backend" there). A `nativeToken` whose backend is Android is an `ANativeWindow*` — an + address in the client's process — and may never cross (§3.4). + +## 2. Ordering + +1. The channel keeps its P5 semantics: one blocking slot, `m_callerMutex` serialising posters, + pumped by the apply thread **between drain batches**, publish-then-ring. Frames are FIFO + against each other; they are NOT ordered against SEG_CMD records by the channel itself. +2. The client-side discipline that orders frames against records is unchanged and remains + mandatory under spawn: `WaitForApplyBeforeEglForwarder` waits `appliedSeq` to + `LastPublishedSeq` before every forwarder (CONTRACT-P5E §2.5), because the channel is pumped + between drain batches and a control frame would otherwise overtake in-flight records. +3. **All twelve ops stay blocking in fc**, including the three f0-egl §4.3 marked relaxable + (`SetSwapInterval`, `ReleaseSurface`, `SetWindowHandle`). Relaxing them to fire-and-forget is + a P6 decision and must preserve: a `SetWindowHandle` is in effect before any later + `CreateWindowSurface` (one ordered channel suffices), and `ReleaseResources` NEVER relaxes — + `MobileGL::Destroy()` walks on the moment it answers (ServerLoop.h's header note). +4. Re-entrancy is unchanged: a post from the apply thread dispatches inline (the + `~BackendObject_DirectGLES` → `ReleaseEGLResources` path). This is thread semantics, not wire + semantics; under spawn the destructor path runs server-side natively and never posts. + +## 3. Error semantics + +1. Loop down, backend alive → `Fatal{ApplyThreadNotRunning}` (M-7, unchanged). +2. Loop down, no backend → `MOBILEGL_ERR_NOT_INITIALIZED`, nothing runs on the caller (C2, + unchanged). +3. Backend null at dispatch → `MOBILEGL_ERR_NOT_INITIALIZED` with `ok=false`; the forwarder's + answer is the old `false`/void-drop, unchanged. +4. Wire entry (`ServerApplyWireSurfaceOp`, the future P6 control pump's entry point; P5f drives + it from tests only): a decode failure is **`Fatal{ProtocolCorruption, "SurfaceOp"}`**, never a + dropped frame (a dropped control frame is a client waiting on a reply that never comes); a + decoded `WindowKind::AndroidNativeWindow` is + **`Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`** — the window kind is legal schema, + the token is the part that means nothing in the server's process, and real window arrival is + P12. Inproc-only kinds (`InitCapabilitiesInprocOnly`, `SwapBuffersInprocOnly`, + `InitWindowSurfaceInprocOnly`, `ProbeForTesting`) cannot arrive: the encoder refuses them + (`SurfaceWireError::InprocOnlyOpOnTheWire`). +5. A frame that reaches the inproc dispatch with `kind == None` or a kind the switch does not know + is `Fatal{ProtocolCorruption, "SurfaceOp.kind"}`; a `windowBackend` tag outside the enum is + `Fatal{ProtocolCorruption, "SurfaceOp.windowBackend"}`. Inproc these are corruption shapes, not + bad input — same discipline as the ring's header check. + +## 4. Reply carriers — and the deliberate non-use of `SurfaceReply.defaultFb` + +1. The default framebuffer's shape already crosses as `kEventSurfaceChanged` on SEG_EVENT (P5c + ev), and the client drains it at the RPC return (`BackendObject_Remote.cpp`'s create/resize + arms). fc rules that `SurfaceReply.defaultFb` **stays unset**: two spellings of one fact is how + the two sides come to disagree about it (the `CapsSnapshot` deprecated-fields lesson, + `protocol.fbs`). Enabling it later is a schema amendment, not a silent flip. +2. The caps republish side effects are unchanged and are server-spontaneous frames on the wire: + a real bind under `MakeCurrent` republishes (R-12 arm (a)); `InitCapabilities` publishes. The + client's absorption point — pump the control plane after the RPC returns — is the same in both + worlds: "after this op's reply, before the next op" is a total order the client can rely on. + +## 5. What fc deliberately did NOT do + +- No transport: no socket, no spawn pump, no client-minted token table (the frame fields are + token-width already, so P6 is transport-only here). +- The two dead forwarders (`ServerSwapEGLBuffers`, `ServerInitWindowSurface`) and + `ServerInitCapabilities` were NOT given wire ops (f0-egl F8/§4.2): they ride the inproc channel + as inproc-only kinds because the function-pointer mailbox is gone entirely, and G14 forbids + deleting their pinning tests. +- Blocking semantics, the shadow, the doorbell order and the C2 exit block: unchanged. diff --git a/docs/Disaggregated/notes/p5f/fc-report.md b/docs/Disaggregated/notes/p5f/fc-report.md new file mode 100644 index 00000000..ed0ca2a9 --- /dev/null +++ b/docs/Disaggregated/notes/p5f/fc-report.md @@ -0,0 +1,117 @@ +# P5f / fc — EGL/surface 控制面帧化:落地报告 + +> 落地于 `p5f/fc`。设计输入:`f0-egl.md`(逐 forwarder 点名表、schema 缺口清单、帧设计草案); +> 契约草稿 [`fc-contract-draft.md`](fc-contract-draft.md)(CONTRACT-P5F.md 本体留给集成收口)。 +> 范围:P5F-WIRE-COMPLETENESS.md §2.3(P5c 审计 G4 行,原记 P6,P5f 收回)。 + +--- + +## 1 落地内容 + +| 文件 | 内容 | +|---|---| +| `MobileGL/MG_Remote/Protocol/protocol.fbs` | **append-only**:`SurfaceOpKind` 补 `SetSwapInterval=8` / `ReleaseResources=9` / `SetWindowHandle=10`(与 P6-SPAWN-PLAN 的预测逐字一致);`WindowKind` 补 `MetalLayer=6`(f0-egl schema 缺口 3:两枚举不互洽的另一半);`SurfaceOp` 补 `readSurface`、`context` 两字段(MakeCurrent 的语义单元是四元组) | +| `.../Protocol/generated/protocol_generated.h` | 用 pinned flatc(`~/w7/p5-s1-flatc-build/flatc`,25.12.19)重新生成;diff 仅 +58/−6 行,全部落在追加的枚举值与字段上 | +| `.../Server/SurfaceControlFrame.{h,cpp}` | 纯值帧:`SurfaceControlOp`(10 个 wire op + 4 个 inproc-only kind)+ `SurfaceControlFrame`(`static_assert` 钉死 trivially-copyable / standard-layout——帧里**不可能**有指针) | +| `.../Protocol/SurfaceOpCodec.{h,cpp}` | 帧 ↔ `Wire::SurfaceOp`/`SurfaceReply` 编解码;`WindowBackend` ↔ `WindowKind` 显式映射表(Surfaceless/Pbuffer 具名无后端);wire 入口 `ServerApplyWireSurfaceOp` 的两个具名 Fatal(见 §3);枚举对齐用 static_assert 钉死而非强转 | +| `.../Server/ServerLoop.{h,cpp}` | **单槽邮箱的载荷从"函数指针 + 栈上 void\*"换成按值携带的一个帧**。`RunOnApplyThread(ControlWork, void*)` → `RunSurfaceControlFrame(SurfaceControlFrame&)`;十二个 forwarder 的 Args::Run 本体逐字搬进成员分发 `ApplySurfaceControlFrame`(C7/ID-54 分类、N-3 遗忘、R-12 重发布全部原位保留);阻塞握手、影子位、publish-then-ring、C2 出口块全部不动。测试缝:`RunProbeOnApplyThreadForTesting`(hook 是成员变量,**不进槽位**) | +| `MG_Test/Wire/SurfaceControlFrameTest.cpp` | 新套件 9 用例(见 §3) | +| `MG_Test/Wire/ServerLoopTest.cpp` | 既有用例 5 处迁移到 probe 缝;新增 2 用例(见 §3) | +| `MG_Test/Wire/RemoteClientTest.cpp` + `RemoteClientControls.inc` | 18 + 11 处 `RunOnApplyThread` 调用点机械迁移到 probe 缝(lambda 签名逐字兼容) | +| 根 `CMakeLists.txt` / `MG_Test/Wire/CMakeLists.txt` | 两个新源文件进 disagg 闭包;新套件按既有"加一个 NAME"约定注册 | +| 文档 | ROADMAP.md G4 行(f0-egl F9 的行号修正 + 去向改为"已由 P5f fc 帧化");P5F 计划进度行与 §2.3 标注;P6-CONTRACT-DRAFT §1/§4 与 P6-SPAWN-PLAN 第 4 条改为"fc 已落,P6 只剩传输";CONTRACT-P5E 两处 ServerLoop.cpp 行号随动 | + +**grep 证明**(验收项):`grep -rn "ControlWork\|m_controlWork\|m_controlUser\|RunOnApplyThread" +MobileGL/ --include="*.h" --include="*.cpp" --include="*.inc"` → **0 命中**;邮箱槽位类型是 +`SurfaceControlFrame m_controlFrame;`(按值,`ServerLoop.h:412`),其 trivially-copyable 由 +`SurfaceControlFrame.h` 的 static_assert 钉死。披露:测试缝的 `m_controlProbeHook` / +`m_controlProbeUser` 是 ServerLoop 的**成员**而非槽位内容(同 `m_beforeRetireHook` 的既有先例), +只在 `ProbeForTesting` kind 的分发臂被读。 + +### 与 f0-egl 的逐条核对 + +- **9 个 forwarder / 10 种 op 帧化** ✔(含 `ReleaseCurrent` 复用:`ServerMakeEGLCurrent` 在打包侧按 + 三 NO\_\* 形状拆出 `ReleaseCurrent` kind,分发到达同一个 `ApplyMakeCurrent` ClientRelease 臂)。 +- **死代码不帧化** ✔(按 F8 的"标注 inproc-only"臂):`ServerSwapEGLBuffers` / `ServerInitWindowSurface` + 与 `ServerInitCapabilities` 拿的是 **inproc-only kind,codec 拒绝编码它们** + (`InprocOnlyOpOnTheWire`)。它们仍走帧通道是因为函数指针邮箱已整体拆除,而 G14 禁止删它们的 + 钉住用例。这是与 f0-egl 字面建议的唯一偏差,理由是它自己的 F8 给的第二条路。 +- **`ANativeWindow*` 具名拒绝** ✔:wire 解码入口 `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}` + (P6-CONTRACT-DRAFT 预写的名字);inproc 路径不受影响(Android 同进程照常)。 +- **`SurfaceReply.defaultFb` 不启用** ✔(f0-egl §4.4 路线冲突的二选一):默认 FBO 形状已由 + SEG_EVENT `kEventSurfaceChanged` 承载,回复帧不回写它。契约草稿 §4。 +- **发送纪律**(f0-egl §3):`WaitForApplyBeforeEglForwarder` 原样保留;契约草稿 §2.2 写明它在 + spawn 下仍是硬要求。 + +## 2 双块车道:fc 名下的红 + +**零。** `dualblock-expected-fatals.txt` 的 6 对首阻塞全部是 `PipeInputs` 字段的 BARRIER_PULLED +读(GetFramebufferBindingSlot@ReadPixels 等),与 EGL 控制面无交集——双块机关探的是 +`gPipeInputs` 的读侧,邮箱/帧通道不经过它。fc 的对应义务是**普查不变**:旋钮开的车道在 fc 前后 +产生同一对集(逐门数字见 §5),棘轮文件未动。fc 自己的红一次对照见 §4。 + +## 3 新增测试(G14:只增不删) + +`SurfaceControlFrameTest`(9):帧是纯值聚合(trivially-copyable);十个帧化 op 恰好有 wire +kind 且数值钉死(8/9/10 与 MetalLayer=6 是 wire ABI);WindowBackend↔WindowKind 双向显式表 +(含"两枚举整数值错位"的防强转断言);十 op 逐个过真 `CtrlEnvelope{SurfaceOp}` 字节往返 +(readSurface/context 在内);SurfaceReply 往返(seq 回声 + 版本号);inproc-only 与畸形帧的 +编码拒绝;无后端 windowKind 的解码拒绝;**两个 fork 死亡用例**钉住 wire 入口的具名 Fatal。 + +`ServerLoopTest` +2:`AVoidForwarderCrossesAsOneDispatchedFrame`(无 backend 时 +`ServerSetEGLSwapInterval` 仍恰好分发一帧——帧通道自己的 R-16 扳机); +`ServerLoopEglTest.TheInitializeDisplayReplyArrivesThroughTheFrame`(major/minor 以**回复字段** +而非栈指针回到调用方,mesa 真实版本号佐证)。 + +既有钉住用例全部保留并经帧路径驱动:C7 的 native bind 计数(`readSurface`/`context` 字段若丢, +0x1 句柄会变 0 → ClientRelease 误分类 → NativeBindCount 不动 → 红)、N-3 的 destroy-recreate、 +ID-67 的重发布计数、M-7/C2 的两个 !m_running 臂、影子位双清用例。 + +## 4 R-16 阴性对照(真跑过一次) + +在 WSL 门树(未提交)把 `ServerSetEGLSwapInterval` 的投递行改成 `(void)frame;`(forwarder 不再 +过帧通道),重链 ServerLoopTest: + +- **红**:`ServerLoopTest.AVoidForwarderCrossesAsOneDispatchedFrame` Failed,失败文本正是该用例 + 自己的断言——`loop.ControlFramesDispatched()` Which is: **0**,expected 1 + ("the forwarder reached the backend (or did nothing) without crossing the frame channel")。 +- 还原后同一条目转绿(`100% tests passed, 0 tests failed out of 1`)。 + +## 5 逐门数字(WSL 门树 `~/w7/p5f-fc`,split flavour,逐条实跑) + +| 门 | 结果 | +|---|---| +| build(split flavour) | rc=0(全量 915+ TU;增量修订后最终全绿) | +| unit(旋钮关) | **2273/2273**(f1 基线 2262 + 本包新增 11:SurfaceControlFrameTest 9 + ServerLoopTest 2;G14 只增不删,无标签前缀误伤——未新增任何标签,`SurfaceControlFrameTest` 名集合与既有名无前缀关系) | +| integration-split(旋钮关) | **179/179**,零回归 | +| strict(旋钮关) | 179/179 绿、`Fatal{` 为 0、观测 Admitted 恰为既有 8 对(7 admitted + 1 escalated)。双向棘轮仍报 f1 §8.1 记录的**基线既有** 2 行 stale(`GetBoundVertexArray@DrawArrays`、`GetBufferBindingSlot@DrawArrays`)——与 fc 无关,f1 已归因基线树逐字复现,修复属 strict 车道养护 | +| 双块 census(旋钮开) | 181 条目:**155 红 / 22 绿 / 4 skip,与 f1 初版逐对逐数相同**(132/11/6/2/2/2 六对),`expect-fatal: ratchet OK - 6 fatal pair(s), zero admitted, every red entry named`。**棘轮文件零改动**——fc 名下无红(§2) | +| R-16 阴性对照 | 见 §4(改回即红、还原即绿,各真跑一次) | +| flavours | pull rc=0(915 TU,`MOBILEGL_PIPE_PUSH=1` absent ✓)、push rc=0(929 TU,=1 ✓),compile_commands.json 断言过 | +| **G1**(pull 对基线 `~/w7/p5f-base` @ e75e00cb) | `.text` 10806051 → 10806051(+0);defined symbols 27815 → 27815,**0 added / 0 removed / 0 resized / 0 renamed**——零认定 | +| gens | gen_pipe --check/--self-test、field-ownership(fc 未动 FieldOwnership.def,仍跑)--check/--self-test、dirty-surface --check/--self-test、include 闭包 4 probes 全绿;**protocol 重生成对提交头零 diff**(pinned flatc 25.12.19);doc 引用 --strict 恰为基线既有 2 处 ambiguous(CONTRACT-P5.md:349/:524,f1 §8.2 同此),fc 新增 0 处 | +| integration-gpu | 按任务分工由集成者跑,本包未跑 | + +注:f1 §8.4 的教训已遵守——census 与其他车道串行跑,无私有日志覆写。 + +## 6 附带发现(未修) + +1. `p5f_new.sh` 按 SHA fetch 在源仓未开 `uploadpack.allowAnySHA1InWant` 时失败 + ("couldn't find remote ref");fc 改用按分支名 fetch(`git fetch p5f/fc`)。 + 建议脚本照改或源仓开 `allowTipSHA1InWant`。 +2. `git submodule update --init --recursive` 在新建 worktree 上反复超时(嵌套子模块走网络); + fc 从同父提交的 `~/w7/p5f-f1` 树整体拷贝 submodule 工作区并改写 gitdir 指针解决。 + 脚本那行 `cp -a .../glslang/External` 掩盖了这一点(它只补了 glslang 一家)。 +3. `p5f_gate.sh` 的 build 步以 `[ $rc -ne 0 ] && grep ...` 收尾,成功时脚本退出码是 1 + (日志行 `build rc=0` 才是真相)。既有噪音,未动。 + +## 7 预期合并冲突点 + +- `MobileGL/MG_Test/Wire/CMakeLists.txt` 的套件清单(文件自述为"本阶段反复冲突点",按 + "加一个 NAME" 约定解)。 +- `ServerLoop.{h,cpp}` 与 `RemoteClient{Test,Controls}`:若有并发包碰 apply 线程或角色守卫 + 测试(fm 的 Clear 臂不改这里;fv 若动 `RunOnApplyThread` 调用点则须平移到 probe 缝)。 +- `protocol.fbs` 与 `protocol_generated.h`:若并发包也动 schema,枚举/字段追加序须人工保 + append-only 后重生成。 +- `docs/Disaggregated/ROADMAP.md` G4 行、`P5F-WIRE-COMPLETENESS.md` 进度行、`P6-CONTRACT-DRAFT.md` + §4:文档同点编辑。