mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Remote, MG_Test] (Disaggregated): P5f fc - the EGL control plane crosses as value frames, not function pointers
The twelve Server* forwarders' one-slot mailbox carried a raw function
pointer plus a void* to a stack-local Args struct (P5c audit row G4);
neither means anything across a process boundary. The slot now carries
one SurfaceControlFrame by value: kind is the op, scalars are the
arguments, and ok/eglMajor/eglMinor come back in the same value. The
blocking handshake, the shadow, the doorbell order and the C2 exit
block are unchanged.
- protocol.fbs: append-only - SurfaceOpKind gains SetSwapInterval=8,
ReleaseResources=9, SetWindowHandle=10; WindowKind gains MetalLayer=6;
SurfaceOp gains readSurface and context (the MakeCurrent four-tuple).
protocol_generated.h regenerated with the pinned flatc.
- Server/SurfaceControlFrame.{h,cpp}: the frame (trivially copyable by
static_assert) and the op enum - ten wire kinds plus four inproc-only
kinds (InitCapabilities is answered by CapsSnapshot on the wire,
SwapBuffers/InitWindowSurface have no production caller - f0-egl F8,
kept for the inproc test lane).
- Protocol/SurfaceOpCodec.{h,cpp}: the frame <-> SurfaceOp/SurfaceReply
codec, the explicit WindowBackend <-> WindowKind table, and the wire
entry ServerApplyWireSurfaceOp whose refusals are named:
Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} and
Fatal{ProtocolCorruption, "SurfaceOp"}.
- ServerLoop: RunOnApplyThread(fn, void*) -> RunSurfaceControlFrame(frame)
plus the dispatch ApplySurfaceControlFrame; the old per-forwarder Args
bodies moved into the dispatch unchanged (C7/ID-54, N-3, R-12 included).
RunProbeOnApplyThreadForTesting is the test seam through the same
channel (the hook lives in a member, never in the slot).
- Tests: RemoteClientTest/ServerLoopTest migrated to the probe seam; new
SurfaceControlFrameTest suite (mapping tables, wire round-trips, the
two named refusals) plus ServerLoopTest's AVoidForwarderCrossesAsOne-
DispatchedFrame (the frame channel's red-once handle) and
ServerLoopEglTest's TheInitializeDisplayReplyArrivesThroughTheFrame.
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`)与
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 出口门
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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:文档同点编辑。
|
||||
Reference in New Issue
Block a user