Merge commit 'd5ed139cc50f75a6e7f20eb0cff4a36c7e8b46be' into codex/p5f-wave2

This commit is contained in:
2026-09-19 21:52:55 -04:00
24 changed files with 1664 additions and 336 deletions
+4
View File
@@ -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()
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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
@@ -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
@@ -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
@@ -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 <MG_Backend/BackendObject.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
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<Uint8>(SurfaceControlOp::InitializeDisplay) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::InitializeDisplay));
static_assert(static_cast<Uint8>(SurfaceControlOp::CreateWindowSurface) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface));
static_assert(static_cast<Uint8>(SurfaceControlOp::CreatePbufferSurface) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreatePbufferSurface));
static_assert(static_cast<Uint8>(SurfaceControlOp::ResizeWindowSurface) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ResizeWindowSurface));
static_assert(static_cast<Uint8>(SurfaceControlOp::ReleaseSurface) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseSurface));
static_assert(static_cast<Uint8>(SurfaceControlOp::MakeCurrent) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::MakeCurrent));
static_assert(static_cast<Uint8>(SurfaceControlOp::ReleaseCurrent) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseCurrent));
// fc's three schema additions, append-only (protocol.fbs's own rule).
static_assert(static_cast<Uint8>(SurfaceControlOp::SetSwapInterval) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval));
static_assert(static_cast<Uint8>(SurfaceControlOp::ReleaseResources) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources));
static_assert(static_cast<Uint8>(SurfaceControlOp::SetWindowHandle) ==
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle));
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval) == 8 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources) == 9 &&
static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::SetWindowHandle) == 10);
static_assert(static_cast<Uint8>(::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 "<unknown SurfaceWireError>";
}
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<Uint8>(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<SurfaceControlOp>(static_cast<Uint8>(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<MG_Backend::WindowBackend>(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<Int>(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<unsigned>(op.kind()),
static_cast<unsigned>(op.windowKind()));
}
std::abort();
}
const MobileGLResult rc = Server::ServerLoopInstance().RunSurfaceControlFrame(frame);
if (replyOut != nullptr) *replyOut = frame;
return rc;
}
} // namespace MobileGL::MG_Remote
@@ -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 <Includes.h>
// 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 <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Protocol/mg_protocol_base.h>
#include <MG_Remote/Server/SurfaceControlFrame.h>
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
@@ -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<size_t>(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<size_t>(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<uint64_t>(VT_SEQ, 0);
@@ -1081,6 +1095,12 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
int32_t swapInterval() const {
return GetField<int32_t>(VT_SWAPINTERVAL, 0);
}
uint64_t readSurface() const {
return GetField<uint64_t>(VT_READSURFACE, 0);
}
uint64_t context() const {
return GetField<uint64_t>(VT_CONTEXT, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -1093,6 +1113,8 @@ struct SurfaceOp FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyField<int32_t>(verifier, VT_WIDTH, 4) &&
VerifyField<int32_t>(verifier, VT_HEIGHT, 4) &&
VerifyField<int32_t>(verifier, VT_SWAPINTERVAL, 4) &&
VerifyField<uint64_t>(verifier, VT_READSURFACE, 8) &&
VerifyField<uint64_t>(verifier, VT_CONTEXT, 8) &&
verifier.EndTable();
}
};
@@ -1128,6 +1150,12 @@ struct SurfaceOpBuilder {
void add_swapInterval(int32_t swapInterval) {
fbb_.AddElement<int32_t>(SurfaceOp::VT_SWAPINTERVAL, swapInterval, 0);
}
void add_readSurface(uint64_t readSurface) {
fbb_.AddElement<uint64_t>(SurfaceOp::VT_READSURFACE, readSurface, 0);
}
void add_context(uint64_t context) {
fbb_.AddElement<uint64_t>(SurfaceOp::VT_CONTEXT, context, 0);
}
explicit SurfaceOpBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -1149,8 +1177,12 @@ inline ::flatbuffers::Offset<SurfaceOp> 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);
+14
View File
@@ -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 {
+370 -243
View File
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> callerLock(m_callerMutex);
std::unique_lock<std::mutex> 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<std::mutex> 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 <class Args>
MobileGLResult RunOnApply(Args& args) {
return ServerLoopInstance().RunOnApplyThread(
+[](void* user) -> MobileGLResult { return static_cast<Args*>(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 <class Handle>
Uint64 TokenFromHandle(Handle handle) {
static_assert(std::is_pointer_v<Handle>, "an EGL handle is a pointer");
return static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(handle));
}
template <class Handle>
Handle HandleFromToken(Uint64 token) {
static_assert(std::is_pointer_v<Handle>, "an EGL handle is a pointer");
return reinterpret_cast<Handle>(static_cast<std::uintptr_t>(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<Int>(handle.Backend);
frame.nativeToken = TokenFromHandle(handle.Handle);
frame.width = static_cast<Int>(handle.Width);
frame.height = static_cast<Int>(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<Int>(MG_Backend::WindowBackend::Unknown) ||
value >= static_cast<Int>(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<MG_Backend::WindowBackend>(value);
}
MG_Backend::WindowHandle UnpackWindowHandle(const SurfaceControlFrame& frame) {
MG_Backend::WindowHandle handle;
handle.Backend = WindowBackendFromFrameValue(frame.windowBackend);
handle.Handle = HandleFromToken<void*>(frame.nativeToken);
handle.Width = static_cast<Uint32>(frame.width);
handle.Height = static_cast<Uint32>(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<EGLDisplay>(frame.display),
&major, &minor);
frame.eglMajor = major;
frame.eglMinor = minor;
return MOBILEGL_OK;
}
case SurfaceControlOp::CreateWindowSurface: {
frame.ok = backend->CreateEGLWindowSurface(HandleFromToken<EGLSurface>(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<EGLSurface>(frame.surface),
static_cast<Uint32>(frame.width),
static_cast<Uint32>(frame.height));
return MOBILEGL_OK;
case SurfaceControlOp::CreatePbufferSurface:
frame.ok = backend->CreateEGLPbufferSurface(HandleFromToken<EGLSurface>(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<EGLDisplay>(frame.display),
HandleFromToken<EGLSurface>(frame.surface),
HandleFromToken<EGLSurface>(frame.readSurface),
HandleFromToken<EGLContext>(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<int>(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<EGLDisplay>(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<EGLSurface>(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<EGLDisplay>(frame.display),
HandleFromToken<EGLSurface>(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<int>(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<Int>(width);
frame.height = static_cast<Int>(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<int>(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
+56 -13
View File
@@ -71,6 +71,7 @@
#include <Includes.h>
#include "ServerSession.h"
#include "SurfaceControlFrame.h"
#include <atomic>
#include <condition_variable>
@@ -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<MG_Backend::BackendObject> 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<Bool> 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<Uint64> m_controlSeq{0};
std::atomic<Uint64> 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<ControlProbeHook> m_controlProbeHook{nullptr};
std::atomic<void*> 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);
@@ -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 "<unknown SurfaceControlOp>";
}
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
@@ -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 <Includes.h>
#include <type_traits>
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<SurfaceControlFrame> &&
std::is_standard_layout_v<SurfaceControlFrame>,
"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
+4 -1
View File
@@ -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
+11 -11
View File
@@ -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<FenceBudgetPeer*>(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<ReadPeer*>(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<ErrorPeer*>(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<CapsPeer>();
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<CapsPeer>();
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<CapsPeer*>(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<DrawPeer*>(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;
+18 -18
View File
@@ -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<F1Peer*>(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<MG_State::GLState::GLContext>();
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<MG_State::GLState::GLContext>();
TestTextureTable table;
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(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<MG_State::GLState::BufferObject*>(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<MG_State::GLState::BufferObject*>(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<Probe*>(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<MG_State::GLState::TextureObject2D*>(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<PresentCreditPeer*>(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,
+50 -8
View File
@@ -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<Probe*>(user);
p->ranOn = std::this_thread::get_id();
@@ -375,7 +375,7 @@ TEST(ServerLoopTest, TheControlShadowIsClearedWhenThePumpTakesTheRequest) {
std::atomic<Bool> answered{false};
MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT;
std::thread poster([&] {
rc = loop.RunOnApplyThread(
rc = loop.RunProbeOnApplyThreadForTesting(
+[](void* user) -> MobileGLResult {
Gate* g = static_cast<Gate*>(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<Outer*>(user);
o->innerRc = Server::ServerLoopInstance().RunOnApplyThread(
o->innerRc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* inner) -> MobileGLResult {
*static_cast<Bool*>(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<Probe*>(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<void()>& body) {
std::function<void()> copy = body;
return Server::ServerLoopInstance().RunOnApplyThread(
return Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* user) -> MobileGLResult {
(*static_cast<std::function<void()>*>(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,
@@ -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 <MG_Remote/Protocol/SurfaceOpCodec.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/SurfaceControlFrame.h>
#include <gtest/gtest.h>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <string>
#include <type_traits>
#if defined(_WIN32)
#include <process.h>
#else
#include <unistd.h>
#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<char>(in), std::istreambuf_iterator<char>());
}
unsigned ProcessId() {
#if defined(_WIN32)
return static_cast<unsigned>(_getpid());
#else
return static_cast<unsigned>(::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<SurfaceControlFrame>)
<< "the frame grew a non-trivial member; the one-slot channel copies it by value";
EXPECT_TRUE(std::is_standard_layout_v<SurfaceControlFrame>)
<< "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<unsigned>(kind), static_cast<unsigned>(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<unsigned>(::MobileGL::Wire::SurfaceOpKind::SetSwapInterval), 8u);
EXPECT_EQ(static_cast<unsigned>(::MobileGL::Wire::SurfaceOpKind::ReleaseResources), 9u);
EXPECT_EQ(static_cast<unsigned>(::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<unsigned>(::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<int>(WindowBackend::Android),
static_cast<int>(::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<Int>(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;
}
+7 -1
View File
@@ -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`)与
+24 -14
View File
@@ -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
+2 -2
View File
@@ -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 出口门
+2 -2
View File
@@ -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)。
## 里程碑
@@ -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.
+117
View File
@@ -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 <gitdir> 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:文档同点编辑。