[Feat] (MG_Remote, Client, Server): the Hello/Welcome/CapsSnapshot handshake and both sessions' construction and teardown - the ABI assertion runs in Accept before a record is decoded and is Fatal{AbiMismatch} rather than a downgrade, and the two doorbell accessors live on the session rather than on ITransport

This commit is contained in:
2026-09-11 14:13:20 -04:00
parent 7c25130d5a
commit 558fb210dc
6 changed files with 1033 additions and 26 deletions
+32 -1
View File
@@ -8,6 +8,9 @@
#include "CapsCodec.h"
#include "Transport/SessionRings.h"
#include <MGGitHash.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
@@ -49,7 +52,35 @@ namespace MobileGL::MG_Remote {
Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); }
Uint64 CapsAbiFingerprint() { MGP5_C0_STUB("CapsAbiFingerprint"); }
// s1's half of this file (the two codecs above stay w1's).
//
// MGPCaps has only a COMPOSITIONAL size assertion (MGPipeTypes.h:145-146),
// because DynamicBackendParameters still carries SizeT and GLenum members -
// P0.5's fixed-width rewrite never happened and P5 does not do it either
// (that is P7's account, CONTRACT-P5 table 0). So the two peers assert they
// were built from the SAME struct shapes instead, and a mismatch is
// Fatal{AbiMismatch}, NEVER a downgrade: every alternative to aborting reads
// one struct as another and produces a plausible picture for the wrong reason.
//
// GLFunctionsTable is in the mix even though a split client never receives
// one, because the SERVER's table shape is what the emit table is derived
// from (R-4's 71 slots) and a peer whose table is a different size has a
// different slot numbering.
//
// The git stamp is the weakest of the four inputs and is here for its
// diagnostic value rather than its strength: it is captured at CMAKE
// CONFIGURE time, so an incremental build after a commit still reports the
// configured hash. The three sizeofs are what actually catch a shape change,
// and under P6's spawn - same machine, same binary - all four are trivially
// equal, which is the case this assertion is cheapest in and least needed.
Uint64 CapsAbiFingerprint() {
return Transport::MixAbiFingerprint(
static_cast<Uint64>(sizeof(MG_Backend::DynamicBackendParameters)),
static_cast<Uint64>(sizeof(MG_Pipe::MGPCaps)),
static_cast<Uint64>(sizeof(MG_Backend::GLFunctionsTable)),
MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR),
GIT_COMMIT_HASH_SHORT);
}
#undef MGP5_C0_STUB
+374 -6
View File
@@ -6,13 +6,22 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5 c0 stubs for packages s1 (construction, handshake) and c1 (barrier, reply read).
// P5: construction, the handshake and lifetime are package s1's; the verb barrier and the
// reply read that sits inside it are package c1's (EmitAndWait below is still c0's stub).
#include "ClientSession.h"
#include "../CapsCodec.h"
#include "../Protocol/generated/protocol_generated.h"
#include "../Server/ServerLoop.h"
#include "../Server/ServerSession.h"
#include "../Transport/InProcessTransport.h"
#include <MGGitHash.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#include <vector>
namespace MobileGL::MG_Remote::Client {
@@ -24,21 +33,350 @@ namespace MobileGL::MG_Remote::Client {
std::abort(); \
} while (0)
namespace {
ClientSession* g_active = nullptr;
// The same bounded handshake deadline the server uses. Bounded, not kWaitForever: a
// bring-up that never answers has to be a red lane rather than a wedged CI job.
constexpr Uint32 kHandshakeTimeoutMs = 5000;
// Teardown's drain. Also bounded, and for the same reason - table 3's order is
// "publish and wait for the server to drain and acknowledge", and a wait with no
// deadline there turns a lost record into a hung process exit.
constexpr Uint32 kDrainTimeoutMs = 5000;
MobileGLResult ReceiveEnvelope(Transport::ITransport& transport, std::vector<Uint8>& out,
Uint32 timeoutMs) {
Uint64 size = 0;
MobileGLMutableByteSpan empty{nullptr, 0};
const MobileGLResult probe = transport.ReceiveFrame(empty, &size, timeoutMs);
if (probe != MOBILEGL_ERR_BUFFER_TOO_SMALL) {
return probe == MOBILEGL_OK ? MOBILEGL_ERR_PROTOCOL_MISMATCH : probe;
}
out.resize(static_cast<SizeT>(size));
MobileGLMutableByteSpan span{out.data(), out.size()};
return transport.ReceiveFrame(span, &size, 0);
}
const ::MobileGL::Wire::CtrlEnvelope* ParseEnvelope(const std::vector<Uint8>& bytes) {
::flatbuffers::Verifier verifier(bytes.data(), bytes.size());
if (!::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) {
return nullptr;
}
if (!::MobileGL::Wire::CtrlEnvelopeBufferHasIdentifier(bytes.data())) {
return nullptr;
}
return ::MobileGL::Wire::GetCtrlEnvelope(bytes.data());
}
[[noreturn]] void FatalAbiMismatch(const char* what, Uint64 ours, Uint64 theirs,
const char* theirStamp) {
MGLOG_F("MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s "
"theirBuild=%s - never a downgrade: the caps block's size is ABI-dependent "
"and every field past the first difference would be read at the wrong offset",
what, static_cast<unsigned long long>(ours),
static_cast<unsigned long long>(theirs), GIT_COMMIT_HASH_SHORT,
theirStamp == nullptr ? "?" : theirStamp);
std::abort();
}
const char* TransportModeName(MG_Config::TransportMode mode) {
switch (mode) {
case MG_Config::TransportMode::Monolith: return "monolith";
case MG_Config::TransportMode::InProcess: return "inproc";
case MG_Config::TransportMode::Spawn: return "spawn";
case MG_Config::TransportMode::UnixSocket: return "unix:";
case MG_Config::TransportMode::NamedPipe: return "pipe:";
}
return "?";
}
} // namespace
// Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to
// install the remote backend object, and that question has a legitimate "no" - it is the
// monolith answer. Every call that PRESUMES a session aborts instead.
ClientSession* ClientSession::Active() { return nullptr; }
ClientSession* ClientSession::Active() { return g_active; }
MobileGLResult ClientSession::Start(MG_Config::TransportMode, const String&) {
MGP5_C0_STUB("ClientSession::Start");
ClientSession& ClientSessionInstance() {
// Leak at exit, deliberately and per ID-8, exactly as ServerSessionInstance does.
static ClientSession* instance = new ClientSession{};
return *instance;
}
void ClientSession::Stop() { MGP5_C0_STUB("ClientSession::Stop"); }
ClientSession::~ClientSession() {
if (m_started) {
Stop();
}
}
Bool ClientSession::Started() const { return m_started; }
MobileGLResult ClientSession::Start(MG_Config::TransportMode mode, const String& endpoint) {
if (m_started) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// A NAMED ERROR, NEVER A FALLBACK TO MONOLITH. A silent fallback here is exactly the
// "the split lane ran monolith and went green" failure the whole phase is built to
// make impossible (ARCHITECTURE.md 10.3), so every mode this build cannot serve is
// refused by name rather than degraded.
if (mode != MG_Config::TransportMode::InProcess) {
MGLOG_E("MG_Remote client: MOBILEGL_TRANSPORT=%s%s is refused by name - P5 implements "
"`inproc` only, and falling back to monolith would make this lane green for "
"the wrong reason. spawn / unix: / pipe: are P6's",
TransportModeName(mode), endpoint.empty() ? "" : endpoint.c_str());
return MOBILEGL_ERR_UNSUPPORTED;
}
// ---- 1. the control plane and the two bells. The transport owns the bells; THE
// SESSION owns the rings, and the accessors stay off ITransport (contract §3.9).
Transport::InProcessTransport::CreatePair(m_clientTransport, m_serverTransport);
m_transport = m_clientTransport.get();
// ---- 2. Hello. Sent before the server accepts: InProcessTransport queues whole
// messages, so one thread can drive both halves of the handshake in order.
const Uint64 fingerprint = CapsAbiFingerprint();
{
::flatbuffers::FlatBufferBuilder builder(512);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto hello = ::MobileGL::Wire::CreateHello(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
/*backendType=*/0u, /*pid=*/0u, /*configBlob=*/0, fingerprint);
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
const MobileGLResult sent = m_transport->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
if (sent != MOBILEGL_OK) {
Stop();
return sent;
}
}
// ---- 3. the server half: ABI assert, four segments, Welcome.
Server::ServerSession& server = Server::ServerSessionInstance();
const MobileGLResult accepted = server.Accept(*m_serverTransport);
if (accepted != MOBILEGL_OK) {
Stop();
return accepted;
}
// ---- 4. Welcome, and this side's half of the ABI assertion.
{
std::vector<Uint8> frame;
const MobileGLResult received = ReceiveEnvelope(*m_transport, frame, kHandshakeTimeoutMs);
if (received != MOBILEGL_OK) {
MGLOG_E("MG_Remote client: no Welcome within %u ms (rc=%d)", kHandshakeTimeoutMs,
static_cast<int>(received));
Stop();
return received;
}
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Welcome) {
MGLOG_E("MG_Remote client: the server's first control frame is not a verifiable "
"Welcome");
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
const ::MobileGL::Wire::Welcome* welcome = envelope->msg_as_Welcome();
const char* theirStamp = welcome->buildFingerprint() == nullptr
? nullptr
: welcome->buildFingerprint()->c_str();
if (welcome->abiFingerprint() != fingerprint) {
FatalAbiMismatch("struct shapes", fingerprint, welcome->abiFingerprint(),
theirStamp);
}
// The four SegmentRefs are what a spawn client MAPS (P6). Under inproc the mapping
// already exists, so what they are good for here is the cross-check that the two
// sides agree about the geometry at all - which is the assertion that would
// otherwise first run in P6, on the day it is expensive to be wrong.
using Slot = Transport::SessionSegmentSlot;
const auto agrees = [&](const ::MobileGL::Wire::SegmentRef* ref, Slot slot,
const char* name) {
if (ref == nullptr || ref->sizeBytes() != server.Shm().AnnouncedSize(slot)) {
MGLOG_E("MG_Remote client: Welcome's %s SegmentRef announces %llu bytes, the "
"server mapped %llu",
name,
static_cast<unsigned long long>(ref == nullptr ? 0 : ref->sizeBytes()),
static_cast<unsigned long long>(server.Shm().AnnouncedSize(slot)));
return false;
}
return true;
};
if (!agrees(welcome->cmdRing(), Slot::Cmd, "cmd") ||
!agrees(welcome->stageRing(), Slot::Stage, "stage") ||
!agrees(welcome->replyPool(), Slot::Reply, "reply") ||
!agrees(welcome->eventRing(), Slot::Event, "event")) {
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
}
// ---- 5. attach to the four segments. Under `inproc` this is the SAME mapping booked
// under the client role; under `spawn` it becomes ShmSegment::Adopt of the fds the
// server passed by SCM_RIGHTS, which is why nothing below this line knows which it was.
const MobileGLResult attached =
m_shm.AttachInProcess(server.Shm(), Transport::MemoryRole::Client);
if (attached != MOBILEGL_OK) {
Stop();
return attached;
}
Transport::RingControl* control = m_shm.CmdControl();
m_cmd = Transport::RingProducer(control, m_shm.CmdRingBase(), m_shm.CmdRingCapacity(),
Transport::RingCursorSet::Cmd);
m_stage = Transport::RingProducer(control, m_shm.StageBase(), m_shm.StageCapacity(),
Transport::RingCursorSet::Stage);
if (!m_cmd.Valid() || !m_stage.Valid()) {
Stop();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// PeerDoorbell() is the bell the SERVER parks on and this side rings; SelfDoorbell() is
// this side's own. Which is which is the session's knowledge, not the transport's.
m_producer.Attach(control, &m_cmd, &m_stage, &m_clientTransport->PeerDoorbell(),
&m_clientTransport->SelfDoorbell(), MG_Config::Ipc.SpinUs);
m_replies = Transport::ReplySlotPool(m_shm.ReplyBase(), m_shm.ReplyBytes(),
m_shm.ReplySlotCount());
m_events = Transport::EventRingConsumer(m_shm.EventControl(), control,
m_shm.EventRingBase(), m_shm.EventRingCapacity(),
m_shm.EventSegmentBase());
if (!m_replies.Valid() || !m_events.Valid()) {
Stop();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_barrierArmed = MG_Config::Ipc.VerbBarrier != 0;
if (!m_barrierArmed) {
MGLOG_W("MG_Remote client: MOBILEGL_IPC_VERB_BARRIER=0 - this is R-1's NEGATIVE "
"CONTROL and is EXPECTED to be red. 31 of the 63 PipeInputs fields are still "
"pulled from a live GLContext by the client's residual fill, so a free-running "
"queue lets the server read a FUTURE value of them");
}
// ---- 6. the client's segment table. IT MUST NOT INSTALL THE PROCESS RESOLVER: there is
// exactly one gMGPipeSegmentResolver per process, the SERVER role owns it (table 3), and
// the client never resolves a span at all - it only ever writes Ptr = nullptr (R-2's
// rule B). Two roles racing on that one inline variable is precisely what the server's
// InstallProcessResolver asserts against.
//
// The Install calls themselves are package w1's and are named-Fatal stubs until w1
// lands; see ServerSession::Accept for why they are called anyway.
m_segments.Install(Wire::kSegCmd,
Wire::SegmentView{m_shm.CmdRingBase(), m_shm.CmdRingCapacity()});
m_segments.Install(Wire::kSegStage,
Wire::SegmentView{m_shm.StageBase(), m_shm.StageCapacity()});
m_segments.Install(Wire::kSegReply,
Wire::SegmentView{m_shm.ReplyBase(), m_shm.ReplyBytes()});
m_segments.Install(Wire::kSegEvent,
Wire::SegmentView{m_shm.EventSegmentBase(),
m_shm.AnnouncedSize(Transport::SessionSegmentSlot::Event)});
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, &m_stage, &m_segments);
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
if (m_transport->PeekFrameSize() != 0) {
std::vector<Uint8> frame;
if (ReceiveEnvelope(*m_transport, frame, 0) == MOBILEGL_OK) {
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (envelope != nullptr &&
envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
// The mirror's Adopt and the two blob DECODERS are c1's and w1's. s1 stops
// at "the snapshot arrived and is verifiable": adopting it here would put
// the caps mirror's invalidation rule (R-12: a second arrival IS the
// invalidation) in two places.
MGLOG_I("MG_Remote client: first CapsSnapshot received (%llu bytes); adopting "
"it is package c1's CapsMirror::Adopt over package w1's decoders",
static_cast<unsigned long long>(frame.size()));
}
}
}
m_started = true;
g_active = this;
LogMemory("handshake");
// ---- 8. and only now the apply thread. It is package v1's ServerLoop: it names the
// thread mgl-srv-apply, applies MOBILEGL_IPC_SERVER_AFFINITY and logs the RESOLVED
// mask. Under `inproc` the client is what starts the server role, which is why this
// call is here rather than in some server-side main.
const MobileGLResult running = Server::ServerLoopInstance().Start(server);
if (running != MOBILEGL_OK) {
Stop();
return running;
}
return MOBILEGL_OK;
}
void ClientSession::Stop() {
if (!m_started) {
// Start's own failure paths land here with a half-built session; tear down what
// exists and leave nothing mapped.
m_producer.Detach();
m_shm.Close();
m_clientTransport.reset();
m_serverTransport.reset();
m_transport = nullptr;
return;
}
// TABLE 3's TEARDOWN ORDER, and every step of it is load-bearing.
//
// 1. publish and let the server drain. Bounded: a lost record must be a red lane, not
// a hung exit.
Transport::RingControl* control = m_shm.CmdControl();
if (control != nullptr) {
const Uint64 submitted = control->submittedSeq.load(std::memory_order_acquire);
m_producer.PublishAndNotify(submitted);
if (submitted != 0 &&
m_producer.WaitForApplied(submitted, kDrainTimeoutMs) != Transport::SessionWait::Reached) {
MGLOG_E("MG_Remote client: the server did not drain to seq %llu within %u ms; "
"tearing down anyway, and anything an emitter still owns is freed below "
"AFTER the join, which is what keeps that from being a use-after-free",
static_cast<unsigned long long>(submitted), kDrainTimeoutMs);
}
}
// 2. Doorbell::Kill(). THE ONLY thing that can wake an apply thread parked on
// kWaitForever (Doorbell.h:211-221): a Notify is consumed by one Park, after which
// Doorbell::Wait re-tests a condition nothing published, finds the bell alive and
// parks again, forever. InProcessChannel::Close kills both bells.
if (m_transport != nullptr) {
m_transport->Shutdown();
}
// 3. JOIN, bounded - package v1's ServerLoop::Stop, which also destroys the server's
// private BackendObject on that thread before it exits.
Server::ServerLoopInstance().Stop();
// 4. and ONLY NOW may anything an emitter owns be released: a var-tail still
// referenced by an unapplied record is a use-after-free the join is what prevents.
LogMemory("teardown");
m_producer.Detach();
m_encoder = Wire::PipeWireEncoder();
m_events = Transport::EventRingConsumer();
m_replies = Transport::ReplySlotPool();
m_cmd = Transport::RingProducer();
m_stage = Transport::RingProducer();
m_shm.Close();
Server::ServerSessionInstance().Close();
m_clientTransport.reset();
m_serverTransport.reset();
m_transport = nullptr;
m_started = false;
if (g_active == this) {
g_active = nullptr;
}
}
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
CapsMirror& ClientSession::Caps() { return CapsMirrorInstance(); }
// PACKAGE c1's. The barrier's wait and the reply's wait are ONE wait (R-3/R-5), which is
// what makes a blocking ReadPixels, MapPersistent's decline and the four Bool acceptances
// cost zero extra round trips - and the client may not re-derive any of those four answers
// locally. s1 supplies the four primitives it composes from: Encoder(), Producer(),
// WaitForApplied() and ReadReply().
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
void*, Uint64, Int32*) {
MGP5_C0_STUB("ClientSession::EmitAndWait");
@@ -47,10 +385,40 @@ namespace MobileGL::MG_Remote::Client {
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
// and an assertion helper that aborts when asked is worse than useless.
// and an assertion helper that aborts when asked is worse than useless. Package c1 gives
// them real answers when it lands the barrier.
Bool ClientSession::InBarrierWait() { return false; }
Bool ClientSession::ApplyThreadIsInsideApplier() { return false; }
Transport::SessionProducer& ClientSession::Producer() { return m_producer; }
Transport::SessionWait ClientSession::WaitForApplied(Uint64 seq, Uint32 timeoutMs) {
return m_producer.WaitForApplied(seq, timeoutMs);
}
Bool ClientSession::ReadReply(Uint64 seq, void* outBytes, Uint64 outCapacity, Int32* outStatus,
Uint64* outSize) {
return m_replies.Read(seq, outBytes, outCapacity, outStatus, outSize);
}
Uint32 ClientSession::MaxReplyBytes() const { return m_replies.MaxReplyBytes(); }
Transport::EventRingConsumer& ClientSession::Events() { return m_events; }
Transport::RingControl* ClientSession::Control() { return m_shm.CmdControl(); }
Transport::SessionSegments& ClientSession::Shm() { return m_shm; }
Transport::ITransport* ClientSession::Control_Plane() { return m_transport; }
Transport::RoleMemorySample ClientSession::SampleMemory() const {
return Transport::SampleRoleMemory(Transport::MemoryRole::Client);
}
void ClientSession::LogMemory(const char* phase) const {
Transport::LogRoleMemory(phase, SampleMemory());
}
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Client
+72
View File
@@ -38,9 +38,22 @@
#include <Config.h>
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Doorbell.h"
#include "../Transport/EventRing.h"
#include "../Transport/ITransport.h"
#include "../Transport/ReplySlot.h"
#include "../Transport/Ring.h"
#include "../Transport/RoleMemory.h"
#include "../Transport/SessionRings.h"
#include "../Wire/PipeWireCodec.h"
#include "CapsMirror.h"
#include <memory>
namespace MobileGL::MG_Remote::Transport {
class InProcessTransport;
}
namespace MobileGL::MG_Remote::Client {
class ClientSession {
@@ -48,6 +61,8 @@ namespace MobileGL::MG_Remote::Client {
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
static ClientSession* Active();
~ClientSession();
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
// - for TransportMode::InProcess - starts the server role's apply thread. Returns a
// named error rather than falling back to monolith: a fallback here is the "split lane
@@ -85,11 +100,68 @@ namespace MobileGL::MG_Remote::Client {
static Bool InBarrierWait();
static Bool ApplyThreadIsInsideApplier();
// ---- s1's additions: the four primitives c1's EmitAndWait composes ---------------
//
// s1 owns construction and lifetime; c1 owns the barrier POLICY. So the plumbing is
// here and the composition is c1's: encode (w1) -> PublishAndNotify -> WaitForApplied
// -> ReadReply. Splitting it the other way round is how a package ends up
// re-deriving an acceptance answer locally, which is the c0f/c0g defect P4a paid two
// contract corrections for.
Bool Started() const;
// Publish the ring head, record submittedSeq, then ring the server IF IT IS PARKED -
// in that order. RingTest.cpp:446 pins the order; SessionProducer is where it lives.
Transport::SessionProducer& Producer();
// Wait for RingControl::appliedSeq >= seq. Returns ShutDown when the doorbell died,
// which is the only thing that returns from a kWaitForever park and therefore the
// only way a client blocked in the barrier survives a server that went away.
Transport::SessionWait WaitForApplied(Uint64 seq, Uint32 timeoutMs);
// The reply slot for `seq`, addressed seq % slots with the seq stamped back into the
// header for self-check (R-3). `outStatus` is 0 OK / 1 DECLINED / 2 ERROR, and
// DECLINED IS A REAL ANSWER - MapPersistent's nullptr and the four Bool acceptances.
Bool ReadReply(Uint64 seq, void* outBytes, Uint64 outCapacity, Int32* outStatus,
Uint64* outSize);
// What one answer may carry. A ReadPixels bigger than this is Fatal rather than
// chunked, so the client checks BEFORE it emits.
Uint32 MaxReplyBytes() const;
// The reverse channel's reading end: OnBufferWriteback / OnGpuWritten /
// OnSurfaceChanged. Drained by the GL thread between verbs.
Transport::EventRingConsumer& Events();
Transport::RingControl* Control();
Transport::SessionSegments& Shm();
Transport::ITransport* Control_Plane();
// Peak-RSS accounting for t1 (RoleMemory.h).
Transport::RoleMemorySample SampleMemory() const;
void LogMemory(const char* phase) const;
private:
Wire::PipeWireEncoder m_encoder;
Wire::SegmentTable m_segments;
CapsMirror* m_caps = nullptr;
Bool m_barrierArmed = true;
std::unique_ptr<Transport::InProcessTransport> m_clientTransport;
std::unique_ptr<Transport::InProcessTransport> m_serverTransport;
Transport::SessionSegments m_shm;
Transport::RingProducer m_cmd;
Transport::RingProducer m_stage;
Transport::SessionProducer m_producer;
Transport::EventRingConsumer m_events;
Transport::ReplySlotPool m_replies;
Transport::ITransport* m_transport = nullptr;
Bool m_started = false;
};
// One per process in P5, because P5 serves one context, and LEAKED AT EXIT like every other
// MG_Remote singleton (ID-8): no frontend destructor may reach pipe or backend state from an
// exit handler, and a session destroyed before them would be a use-after-free rather than a
// tidy teardown. MG_Backend::Init() calls Start() on this one.
ClientSession& ClientSessionInstance();
} // namespace MobileGL::MG_Remote::Client
+17 -1
View File
@@ -10,6 +10,8 @@
#include "PipeApplier.h"
#include "../Transport/ReplySlot.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
@@ -27,7 +29,21 @@ namespace MobileGL::MG_Remote::Server {
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
void ReplyPool::PostReply(Uint64, Int32, const void*, Uint64) { MGP5_C0_STUB("ReplyPool::PostReply"); }
// PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY
// slot pool is s1's deliverable (BRIEF §5) and its addressing lives in one place,
// Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating
// `seq % slots` on this side is how the two halves come to disagree about which slot an
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
// call's answer instead of failing.
//
// The view is rebuilt per call rather than stored, so that this body does not change
// ReplyPool's four members and therefore does not touch v1's header at all.
void ReplyPool::PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) {
Transport::ReplySlotPool pool(m_base, m_size, m_slots);
// Fatal inside Post when the answer does not fit a slot: P5 does not chunk replies,
// and the client knows an answer's size before it emits the record.
pool.Post(seq, status, bytes, size);
}
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
+437 -17
View File
@@ -6,45 +6,465 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5 c0 stubs for package s1.
// P5 package s1: the server half of a session.
#include "ServerSession.h"
#include "../CapsCodec.h"
#include "../Protocol/generated/protocol_generated.h"
#include "../Transport/InProcessTransport.h"
#include <Config.h>
#include <MGGitHash.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#include <vector>
namespace MobileGL::MG_Remote::Server {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedServerSession, \"%s\"} - P5 package s1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
namespace {
ServerSession* ServerSession::Active() { return nullptr; }
// A control-plane frame is small by construction (ITransport.h:56-58: bulk bytes
// belong in shm, never here), so one stack-free vector sized from PeekFrameSize is
// the whole reader. The BUFFER_TOO_SMALL half of ReceiveFrame's contract is what
// makes the two-step safe: a short buffer leaves the message queued.
MobileGLResult ReceiveEnvelope(Transport::ITransport& transport, std::vector<Uint8>& out,
Uint32 timeoutMs) {
Uint64 size = 0;
MobileGLMutableByteSpan empty{nullptr, 0};
const MobileGLResult probe = transport.ReceiveFrame(empty, &size, timeoutMs);
if (probe != MOBILEGL_ERR_BUFFER_TOO_SMALL) {
// OK with a zero-size message, or a real failure. A zero-length control
// frame is not a legal CtrlEnvelope either way.
return probe == MOBILEGL_OK ? MOBILEGL_ERR_PROTOCOL_MISMATCH : probe;
}
out.resize(static_cast<SizeT>(size));
MobileGLMutableByteSpan span{out.data(), out.size()};
return transport.ReceiveFrame(span, &size, 0);
}
MobileGLResult ServerSession::Accept(Transport::ITransport&) { MGP5_C0_STUB("ServerSession::Accept"); }
MobileGLResult SendEnvelope(Transport::ITransport& transport,
::flatbuffers::FlatBufferBuilder& builder) {
return transport.SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
}
// Every message from the peer is verified before a single field is read: the control
// plane is parsed from another process's memory (P6) and from another role's (P5).
const ::MobileGL::Wire::CtrlEnvelope* ParseEnvelope(const std::vector<Uint8>& bytes) {
::flatbuffers::Verifier verifier(bytes.data(), bytes.size());
if (!::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) {
return nullptr;
}
if (!::MobileGL::Wire::CtrlEnvelopeBufferHasIdentifier(bytes.data())) {
return nullptr;
}
return ::MobileGL::Wire::GetCtrlEnvelope(bytes.data());
}
[[noreturn]] void FatalAbiMismatch(const char* what, Uint64 ours, Uint64 theirs,
const char* ourStamp, const char* theirStamp) {
// NEVER a downgrade. Every alternative to aborting here reads one struct as
// another - MGPCaps has only a compositional size assertion because
// DynamicBackendParameters still carries SizeT, so a peer built from a different
// tree hands over a caps block whose members are at different offsets and whose
// bytes are all individually plausible.
MGLOG_F("MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s "
"theirBuild=%s - the two peers were not built from the same struct shapes, "
"and there is no downgrade path: the caps block's size is ABI-dependent "
"(MGPipeTypes.h:145-146) and every field past the first difference would be "
"read at the wrong offset",
what, static_cast<unsigned long long>(ours),
static_cast<unsigned long long>(theirs),
ourStamp == nullptr ? "?" : ourStamp, theirStamp == nullptr ? "?" : theirStamp);
std::abort();
}
Transport::SessionSegmentSizes SizesFromConfig() {
Transport::SessionSegmentSizes sizes;
#if MOBILEGL_BUILD_DISAGGREGATED
const Uint64 ringMb = MG_Config::Ipc.RingMb == 0 ? 8u : MG_Config::Ipc.RingMb;
const Uint64 stageMb = MG_Config::Ipc.StageMb == 0 ? 32u : MG_Config::Ipc.StageMb;
sizes.CmdBytes = ringMb * 1024ull * 1024ull;
sizes.StageBytes = stageMb * 1024ull * 1024ull;
#endif
return sizes;
}
Uint32 SpinUsFromConfig() {
#if MOBILEGL_BUILD_DISAGGREGATED
return MG_Config::Ipc.SpinUs;
#else
return Transport::kDefaultSpinUs;
#endif
}
// The handshake's own deadline. Bounded rather than kWaitForever on purpose: a
// bring-up that never answers must be a red lane, not a wedged CI job - the same
// reason InProcessTransportTest.cpp:344 bounds its join at five seconds.
constexpr Uint32 kHandshakeTimeoutMs = 5000;
// WHICH SUBSYSTEMS THIS SERVER CONSUMES, derived from the only registration the tree
// actually has. PipeFill.cpp:920's P4aFamilyHasItsConsumer is the evidence: every P4a
// family hangs off MGPipeGetResourceOps() and there is deliberately "no per-family
// registration to add". Below that, P2's bits 0..6 are consumed by the APPLIER, which
// a server role has by construction.
//
// FLAGGED FOR v1: this is a default, not a ruling. v1 owns the apply thread and knows
// what its backend actually took over, and SetConsumedSubsystems is how it says so.
// Publishing a bit the server does not consume is ID-39's shape from the other side -
// the client emits and nothing applies - so a wrong answer here is not benign.
Uint64 DeriveConsumedSubsystems() {
if (MG_Pipe::MGPipeGetResourceOps() == nullptr) {
return MG_Pipe::kMGPipeSubsystemsMigratedAtP2;
}
return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a;
}
// The ONE MGPCapBit that is derivable from the server's own registration. Every other
// bit belongs to the package that owns the question it answers; 0 is the honest
// default for those, because a cap bit set on a guess is a capability probe that
// answers "supported" for a path that does not exist (R-15's cross-cutting rule).
Uint64 DeriveCapabilityBits() {
Uint64 bits = 0;
if (MG_Pipe::MGPipeResourceOpsHaveSubDataResident()) {
bits |= static_cast<Uint64>(MG_Pipe::kCapResidentSubData);
}
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes are 0 for the whole of P5 by
// ruling, and that is the cheapest way to keep every MGHostSpan out of the first
// IPC frame: they are the only two things that ask for one.
return bits;
}
ServerSession* g_active = nullptr;
} // namespace
ServerSession& ServerSessionInstance() {
// Leak at exit, deliberately and per ID-8: frontend destructors reach pipe and backend
// state from exit handlers, and a session destroyed before them would be a
// use-after-free rather than a tidy teardown.
static ServerSession* instance = new ServerSession{};
return *instance;
}
ServerSession* ServerSession::Active() { return g_active; }
ServerSession::~ServerSession() { Close(); }
void ServerSession::SetSegmentSizes(const Transport::SessionSegmentSizes& sizes) {
if (m_accepted) {
MGLOG_E("MG_Remote server: SetSegmentSizes after Accept is ignored - the geometry is "
"already on the wire in Welcome and the peer has mapped it");
return;
}
m_sizes = sizes;
}
void ServerSession::SetBackend(MG_Backend::BackendObject* backend) { m_backend = backend; }
void ServerSession::SetCapabilityBits(Uint64 capBits) {
m_capBits = capBits;
m_capBitsSet = true;
}
void ServerSession::SetConsumedSubsystems(Uint64 subsystemMask) {
m_consumedSubsystems = subsystemMask;
m_consumedSet = true;
}
Uint64 ServerSession::CallMask() const {
const Uint64 capBits = m_capBitsSet ? m_capBits : DeriveCapabilityBits();
const Uint64 consumed = m_consumedSet ? m_consumedSubsystems : DeriveConsumedSubsystems();
return capBits | MGCapsConsumerBits(consumed);
}
Bool ServerSession::Accepted() const { return m_accepted; }
MobileGLResult ServerSession::Accept(Transport::ITransport& transport) {
if (m_accepted) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_transport = &transport;
if (m_sizes.CmdBytes == 0) {
m_sizes = SizesFromConfig();
}
// ---- 1/2. the ABI assertion, BEFORE a single record is decoded and before a byte of
// shared memory exists. Its whole purpose is to refuse to interpret the peer's bytes.
std::vector<Uint8> frame;
const MobileGLResult received = ReceiveEnvelope(transport, frame, kHandshakeTimeoutMs);
if (received != MOBILEGL_OK) {
MGLOG_E("MG_Remote server: no Hello within %u ms (rc=%d)", kHandshakeTimeoutMs,
static_cast<int>(received));
return received;
}
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Hello) {
MGLOG_E("MG_Remote server: the first control frame is not a verifiable Hello");
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
const ::MobileGL::Wire::Hello* hello = envelope->msg_as_Hello();
const char* theirStamp =
hello->buildFingerprint() == nullptr ? nullptr : hello->buildFingerprint()->c_str();
if (hello->abiMajor() != static_cast<Uint32>(MOBILEGL_PROTOCOL_ABI_MAJOR) ||
hello->abiMinor() != static_cast<Uint32>(MOBILEGL_PROTOCOL_ABI_MINOR)) {
FatalAbiMismatch("protocol version",
MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR,
MOBILEGL_PROTOCOL_ABI_MINOR),
MOBILEGL_ABI_VERSION(hello->abiMajor(), hello->abiMinor()),
GIT_COMMIT_HASH_SHORT, theirStamp);
}
const Uint64 ourFingerprint = CapsAbiFingerprint();
if (hello->abiFingerprint() != ourFingerprint) {
FatalAbiMismatch("struct shapes", ourFingerprint, hello->abiFingerprint(),
GIT_COMMIT_HASH_SHORT, theirStamp);
}
// ---- 3. the four segments, both control pages, the rings.
const MobileGLResult created = m_shm.Create(m_sizes, Transport::MemoryRole::Server);
if (created != MOBILEGL_OK) {
return created;
}
Transport::RingControl* control = m_shm.CmdControl();
m_commands = Transport::RingConsumer(control, m_shm.CmdRingBase(), m_shm.CmdRingCapacity(),
Transport::RingCursorSet::Cmd);
if (!m_commands.Valid()) {
m_shm.Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_consumer.Attach(control, &m_commands, &ProducerDoorbell(), &ConsumerDoorbell(),
SpinUsFromConfig());
{
Transport::ReplySlotPool pool(m_shm.ReplyBase(), m_shm.ReplyBytes(),
m_shm.ReplySlotCount());
if (!pool.Valid()) {
m_shm.Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// A stale stamp from a previous session must never read as this session's answer.
pool.Clear();
m_replies = ReplyPool(m_shm.ReplyBase(), m_shm.ReplyBytes(), m_shm.ReplySlotCount(),
pool.SlotBytes());
}
m_events = Transport::EventRingProducer(m_shm.EventControl(), control, m_shm.EventRingBase(),
m_shm.EventRingCapacity());
m_applier = PipeApplier(&m_segments, &m_replies);
// ---- 4. Welcome: the four SegmentRefs, plus this side's half of the ABI statement.
{
::flatbuffers::FlatBufferBuilder builder(1024);
using Slot = Transport::SessionSegmentSlot;
const auto segmentRef = [&](Uint32 id, ::MobileGL::Wire::SegmentKind kind, Slot slot) {
return ::MobileGL::Wire::CreateSegmentRefDirect(builder, id, kind,
m_shm.AnnouncedSize(slot),
m_shm.AnnouncedName(slot));
};
auto cmd = segmentRef(1, ::MobileGL::Wire::SegmentKind::Cmd, Slot::Cmd);
auto stage = segmentRef(2, ::MobileGL::Wire::SegmentKind::Stage, Slot::Stage);
auto reply = segmentRef(3, ::MobileGL::Wire::SegmentKind::Reply, Slot::Reply);
auto event = segmentRef(4, ::MobileGL::Wire::SegmentKind::Event, Slot::Event);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto welcome = ::MobileGL::Wire::CreateWelcome(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
static_cast<Uint32>(hello->pid()), cmd, stage, reply, event, stamp, ourFingerprint);
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Welcome, welcome.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
const MobileGLResult sent = SendEnvelope(transport, builder);
if (sent != MOBILEGL_OK) {
m_shm.Close();
return sent;
}
}
// ---- 5. the segment table and the ONE process-wide resolver.
//
// Table 3's ruling: gMGPipeSegmentResolver is a plain non-atomic inline variable and
// there is exactly one per process, so the SERVER role installs it and the client never
// resolves a span at all - it only ever writes Ptr = nullptr. Installed BEFORE the apply
// thread starts, uninstalled after the join and never before.
//
// Both calls are package w1's (Wire/PipeWireCodec.cpp) and are named-Fatal stubs until
// w1 lands. That is deliberate and is where the bring-up currently stops: a session
// that skipped them and carried on would be a decoder with no segments, which is the
// "split lane ran monolith and went green" shape.
m_segments.Install(Wire::kSegCmd,
Wire::SegmentView{m_shm.CmdRingBase(), m_shm.CmdRingCapacity()});
m_segments.Install(Wire::kSegStage,
Wire::SegmentView{m_shm.StageBase(), m_shm.StageCapacity()});
m_segments.Install(Wire::kSegReply,
Wire::SegmentView{m_shm.ReplyBase(), m_shm.ReplyBytes()});
m_segments.Install(Wire::kSegEvent, Wire::SegmentView{m_shm.EventSegmentBase(),
m_shm.AnnouncedSize(
Transport::SessionSegmentSlot::Event)});
m_segments.InstallProcessResolver();
m_accepted = true;
g_active = this;
LogMemory("accept");
// ---- 6. the first CapsSnapshot, if there is a backend to take it from.
if (m_backend != nullptr) {
const MobileGLResult published = PublishCapsSnapshot();
if (published != MOBILEGL_OK) {
return published;
}
} else {
MGLOG_W("MG_Remote server: accepted with NO backend, so the first CapsSnapshot is "
"deferred. Call SetBackend() then PublishCapsSnapshot(). A client that emits "
"before the snapshot arrives reads a placeholder caps mirror");
}
return MOBILEGL_OK;
}
MobileGLResult ServerSession::PublishCapsSnapshot() {
MGP5_C0_STUB("ServerSession::PublishCapsSnapshot");
if (!m_accepted || m_transport == nullptr) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
if (m_backend == nullptr) {
MGLOG_E("MG_Remote server: PublishCapsSnapshot with no backend");
return MOBILEGL_ERR_NOT_INITIALIZED;
}
// The two blob codecs are package w1's (CapsCodec.h). They are named-Fatal stubs
// today; nothing here may substitute a memcpy for them, because both structures hold
// Vectors and Strings and a memcpy of either crosses a host pointer (R-2's rule B).
Vector<Uint8> formats;
Vector<Uint8> renderer;
if (!EncodeFormatCapabilities(m_backend->GetFormatCapabilities(), formats)) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (!EncodeRendererInfo(m_backend->GetRendererInfo(), renderer)) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
const MG_Backend::DynamicBackendParameters& dynamic = m_backend->GetDynamicParameters();
const auto* dynamicBytes = reinterpret_cast<const Uint8*>(&dynamic);
// R-8/C-4: bits 32..47 of CallMask are the CONSUMER MASK, and this is the only place
// they are produced.
const Uint64 callMask = CallMask();
::flatbuffers::FlatBufferBuilder builder(4096);
auto dynamicVector =
builder.CreateVector(dynamicBytes, static_cast<::flatbuffers::uoffset_t>(sizeof(dynamic)));
auto rendererVector = builder.CreateVector(renderer.data(), renderer.size());
auto formatsVector = builder.CreateVector(formats.data(), formats.size());
const RendererInfo& info = m_backend->GetRendererInfo();
auto apiVersion = builder.CreateString(info.RendererGLInfo.TargetGLVersion.toString());
auto snapshot = ::MobileGL::Wire::CreateCapsSnapshot(
builder, dynamicVector, rendererVector, formatsVector, /*extensions=*/0, apiVersion,
callMask, static_cast<Uint32>(m_backend->GetBackendType()));
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::CapsSnapshot, snapshot.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
return SendEnvelope(*m_transport, builder);
}
void ServerSession::Close() {
if (g_active == this) {
g_active = nullptr;
}
if (m_accepted) {
// Uninstall AFTER the apply thread has joined, never before: a record still in
// flight can still resolve a segment offset (table 3's fourth column).
Wire::SegmentTable::UninstallProcessResolver();
}
m_consumer.Detach();
m_commands = Transport::RingConsumer();
m_events = Transport::EventRingProducer();
m_replies = ReplyPool();
m_shm.Close();
m_transport = nullptr;
m_accepted = false;
}
Transport::RingConsumer& ServerSession::CommandRing() { return m_commands; }
Transport::RingControl& ServerSession::Control() { MGP5_C0_STUB("ServerSession::Control"); }
Transport::RingControl& ServerSession::Control() {
Transport::RingControl* control = m_shm.CmdControl();
if (control == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::Control\"} - the control "
"page does not exist until Accept() has mapped SEG_CMD");
std::abort();
}
return *control;
}
Wire::SegmentTable& ServerSession::Segments() { return m_segments; }
PipeApplier& ServerSession::Applier() { return m_applier; }
ReplyPool& ServerSession::Replies() { return m_replies; }
// The bell the apply thread parks on. On the server endpoint of an InProcessTransport that
// is SelfDoorbell(); the client reaches the same bell through its own PeerDoorbell().
Transport::Doorbell& ServerSession::ConsumerDoorbell() {
MGP5_C0_STUB("ServerSession::ConsumerDoorbell");
}
Transport::Doorbell& ServerSession::ProducerDoorbell() {
MGP5_C0_STUB("ServerSession::ProducerDoorbell");
if (m_transport == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no "
"transport; Accept() has not run");
std::abort();
}
if (m_transport->Role() == Transport::TransportRole::InProcess) {
return static_cast<Transport::InProcessTransport*>(m_transport)->SelfDoorbell();
}
// P6: SocketTransport's pair. The accessors stay off ITransport by ruling (contract
// §3.9) precisely so that this stays one switch in one file rather than two virtuals
// every transport has to invent a home for.
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ConsumerDoorbell\"} - transport "
"role %u has no doorbell pair yet; that is P6's SocketTransport",
static_cast<unsigned>(m_transport->Role()));
std::abort();
}
#undef MGP5_C0_STUB
Transport::Doorbell& ServerSession::ProducerDoorbell() {
if (m_transport == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ProducerDoorbell\"} - no "
"transport; Accept() has not run");
std::abort();
}
if (m_transport->Role() == Transport::TransportRole::InProcess) {
return static_cast<Transport::InProcessTransport*>(m_transport)->PeerDoorbell();
}
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"ServerSession::ProducerDoorbell\"} - transport "
"role %u has no doorbell pair yet; that is P6's SocketTransport",
static_cast<unsigned>(m_transport->Role()));
std::abort();
}
Transport::SessionSegments& ServerSession::Shm() { return m_shm; }
Transport::SessionConsumer& ServerSession::Consumer() { return m_consumer; }
Transport::EventRingProducer& ServerSession::Events() { return m_events; }
Transport::ITransport* ServerSession::Control_Plane() { return m_transport; }
void ServerSession::AdvanceCompletedFrame(Uint64 serial) {
if (!m_accepted) {
return;
}
Transport::Watermark::AdvanceCompletedFrame(Control(), serial);
m_consumer.NotifyClient();
}
void ServerSession::ReturnPresentCredit(Uint64 serial) {
if (!m_accepted) {
return;
}
Transport::Watermark::AdvancePresentAck(Control(), serial);
m_consumer.NotifyClient();
}
Transport::RoleMemorySample ServerSession::SampleMemory() const {
return Transport::SampleRoleMemory(Transport::MemoryRole::Server);
}
void ServerSession::LogMemory(const char* phase) const {
Transport::LogRoleMemory(phase, SampleMemory());
}
} // namespace MobileGL::MG_Remote::Server
+101 -1
View File
@@ -12,13 +12,32 @@
// The four segment sizes are already pinned by ProtocolSmokeTest.cpp:72 and are not up for
// re-derivation here: SEG_CMD 8 MiB, SEG_STAGE 32 MiB, SEG_REPLY 8 MiB, SEG_EVENT 256 KiB.
// MOBILEGL_IPC_RING_MB and MOBILEGL_IPC_STAGE_MB move the first two; the ring caps ONE record
// at half its size, so the default 8 MiB caps a record at 4 MiB (R-10).
// at half its size (R-10). See SessionRings.h's header for why the ring inside SEG_CMD is 4 MiB
// rather than 8 - the control page takes the head and the capacity must be a power of two -
// and why that deviation from CONTRACT-P5 §5's arithmetic is in the safe direction.
//
// THE TWO DOORBELL ACCESSORS ARE ON THE CONCRETE CLASS, NOT ON ITransport
// (InProcessTransport.h:64-68). P5 decides this now rather than letting P6 discover it: the
// SESSION owns the pair and hands out references, so ITransport stays the dumb control-plane
// interface its header says it is and SocketTransport does not have to grow two accessors it
// has no natural home for. Discovering this in P6 would mean re-laying one package's call sites.
//
// WHO CREATES THE SEGMENTS: this side. Welcome announces all four SegmentRefs and Welcome is
// server -> client, so the server allocates and the client attaches. "Client-owned" in
// protocol.fbs's comments is about who WRITES a segment, not who allocates it.
//
// WHAT Accept() DOES, IN ORDER. The ABI assertion is FIRST, before a single record is decoded
// and before any segment exists, because its whole purpose is to refuse to interpret the peer's
// bytes at all:
// 1. receive Hello;
// 2. compare abiMajor/abiMinor and CapsAbiFingerprint() - Fatal{AbiMismatch} on a difference,
// never a downgrade;
// 3. create and map the four segments, initialise both control pages, clear the reply pool;
// 4. send Welcome with the four SegmentRefs;
// 5. publish the first CapsSnapshot, IF a backend has been handed over (SetBackend). Without
// one the session is accepted and the snapshot is deferred with a loud line: the caps
// blob codecs are w1's and the server's private BackendObject is v1's, and a session that
// refused to exist until both landed would block every other package's bring-up.
#pragma once
#include <Includes.h>
@@ -26,8 +45,12 @@
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Doorbell.h"
#include "../Transport/EventRing.h"
#include "../Transport/ITransport.h"
#include "../Transport/ReplySlot.h"
#include "../Transport/Ring.h"
#include "../Transport/RoleMemory.h"
#include "../Transport/SessionRings.h"
#include "../Wire/PipeWireCodec.h"
#include "PipeApplier.h"
@@ -37,6 +60,8 @@ namespace MobileGL::MG_Remote::Server {
public:
static ServerSession* Active();
~ServerSession();
// Maps the four segments, answers Hello with Welcome, and publishes the first
// CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is
// decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable)
@@ -62,11 +87,86 @@ namespace MobileGL::MG_Remote::Server {
// cache line otherwise burns a big core for a whole frame on a phone).
Transport::Doorbell& ProducerDoorbell();
// ---- s1's additions beyond c0's signature block ---------------------------------
// The sizes to create the segments with. Must be called before Accept; after it, the
// geometry is on the wire in Welcome and changing it would desynchronise the peer.
void SetSegmentSizes(const Transport::SessionSegmentSizes& sizes);
// The server role's private BackendObject (ServerLoop::Backend(), v1's). It is NOT
// pActiveBackendObject - that global holds the client's BackendObject_Remote (table 3).
// Set before Accept to have the first CapsSnapshot published there; set later and call
// PublishCapsSnapshot yourself.
void SetBackend(MG_Backend::BackendObject* backend);
// MGPCaps::CallMask's two halves, kept apart because they have different owners.
//
// BITS 0..8, THE MGPCapBit FEATURE BITS: nothing in the tree produces them today -
// CallMask is declared at MGPipeTypes.h:133 and written by nobody - and what each one
// answers belongs to the package that owns the question (kCapTimerQuery to the query
// family, kCapResidentSubData to b1, and so on). The default below derives only the
// ONE bit that is mechanically derivable from the server's own registration, and
// kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes stay 0 for the whole of P5 by
// ruling (CONTRACT-P5 table 0), which is what keeps every MGHostSpan out of the first
// IPC frame. Everything else is an owner's to set here.
void SetCapabilityBits(Uint64 capBits);
// BITS 32..47, THE CONSUMER MASK (R-8 / C-4): which MGPipe subsystems this server has
// a consumer for. The client's liveness gates read it back through
// CapsMirror::ServerConsumes and may NEVER read MGPipeGetResourceOps() - that is the
// server's registration, which under inproc a client reads correctly by accident and
// under spawn reads as null, silently disabling five whole record families.
void SetConsumedSubsystems(Uint64 subsystemMask);
// What PublishCapsSnapshot puts on the wire: capBits | MGCapsConsumerBits(subsystems).
Uint64 CallMask() const;
Bool Accepted() const;
// Teardown: after the apply thread has been joined, never before - a record still in
// flight can still resolve a segment offset (table 3's fourth column).
void Close();
Transport::SessionSegments& Shm();
// The apply loop's end of the rings: WaitForWork / ApplyOne / RetireThrough. This is
// the only thing that advances appliedSeq, and it advances it by exactly one per
// record (R-9's ban on batching it while the verb barrier exists).
Transport::SessionConsumer& Consumer();
// The reverse channel. P5 only has to be able to CARRY OnBufferWriteback /
// OnGpuWritten / OnSurfaceChanged; the overflow policy is P9's.
Transport::EventRingProducer& Events();
Transport::ITransport* Control_Plane();
// completedFrameSerial / presentAckSerial: the two watermarks only the server can
// advance, kept together with the other three rather than poked into RingControl from
// whatever code happens to notice a present finished.
void AdvanceCompletedFrame(Uint64 serial);
void ReturnPresentCredit(Uint64 serial);
// Peak-RSS accounting for t1 (RoleMemory.h). `phase` is a short tag.
Transport::RoleMemorySample SampleMemory() const;
void LogMemory(const char* phase) const;
private:
Transport::RingConsumer m_commands;
Wire::SegmentTable m_segments;
PipeApplier m_applier;
ReplyPool m_replies;
Transport::SessionSegments m_shm;
Transport::SessionConsumer m_consumer;
Transport::EventRingProducer m_events;
Transport::ITransport* m_transport = nullptr;
MG_Backend::BackendObject* m_backend = nullptr;
Transport::SessionSegmentSizes m_sizes;
Uint64 m_capBits = 0;
Uint64 m_consumedSubsystems = 0;
Bool m_capBitsSet = false;
Bool m_consumedSet = false;
Bool m_accepted = false;
};
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
// reach pipe or backend state from an exit handler.
ServerSession& ServerSessionInstance();
} // namespace MobileGL::MG_Remote::Server