mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[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:
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
#include "CapsCodec.h"
|
#include "CapsCodec.h"
|
||||||
|
|
||||||
|
#include "Transport/SessionRings.h"
|
||||||
|
|
||||||
|
#include <MGGitHash.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -49,7 +52,35 @@ namespace MobileGL::MG_Remote {
|
|||||||
|
|
||||||
Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); }
|
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
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,22 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// 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 "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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
@@ -24,21 +33,350 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
std::abort(); \
|
std::abort(); \
|
||||||
} while (0)
|
} 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
|
// 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
|
// 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.
|
// 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&) {
|
ClientSession& ClientSessionInstance() {
|
||||||
MGP5_C0_STUB("ClientSession::Start");
|
// 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; }
|
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
|
||||||
|
|
||||||
CapsMirror& ClientSession::Caps() { return CapsMirrorInstance(); }
|
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,
|
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
|
||||||
void*, Uint64, Int32*) {
|
void*, Uint64, Int32*) {
|
||||||
MGP5_C0_STUB("ClientSession::EmitAndWait");
|
MGP5_C0_STUB("ClientSession::EmitAndWait");
|
||||||
@@ -47,10 +385,40 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
||||||
|
|
||||||
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
|
// 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::InBarrierWait() { return false; }
|
||||||
Bool ClientSession::ApplyThreadIsInsideApplier() { 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
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -38,9 +38,22 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Pipe/MGPipe.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 "../Wire/PipeWireCodec.h"
|
||||||
#include "CapsMirror.h"
|
#include "CapsMirror.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Transport {
|
||||||
|
class InProcessTransport;
|
||||||
|
}
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
class ClientSession {
|
class ClientSession {
|
||||||
@@ -48,6 +61,8 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
|
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
|
||||||
static ClientSession* Active();
|
static ClientSession* Active();
|
||||||
|
|
||||||
|
~ClientSession();
|
||||||
|
|
||||||
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
|
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
|
||||||
// - for TransportMode::InProcess - starts the server role's apply thread. Returns a
|
// - 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
|
// 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 InBarrierWait();
|
||||||
static Bool ApplyThreadIsInsideApplier();
|
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:
|
private:
|
||||||
Wire::PipeWireEncoder m_encoder;
|
Wire::PipeWireEncoder m_encoder;
|
||||||
Wire::SegmentTable m_segments;
|
Wire::SegmentTable m_segments;
|
||||||
CapsMirror* m_caps = nullptr;
|
CapsMirror* m_caps = nullptr;
|
||||||
Bool m_barrierArmed = true;
|
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
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
#include "PipeApplier.h"
|
#include "PipeApplier.h"
|
||||||
|
|
||||||
|
#include "../Transport/ReplySlot.h"
|
||||||
|
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -27,7 +29,21 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
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) {}
|
: 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; }
|
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
||||||
|
|
||||||
|
|||||||
@@ -6,45 +6,465 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
// P5 c0 stubs for package s1.
|
// P5 package s1: the server half of a session.
|
||||||
|
|
||||||
#include "ServerSession.h"
|
#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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Server {
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
#define MGP5_C0_STUB(what) \
|
namespace {
|
||||||
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)
|
|
||||||
|
|
||||||
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() {
|
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::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; }
|
Wire::SegmentTable& ServerSession::Segments() { return m_segments; }
|
||||||
PipeApplier& ServerSession::Applier() { return m_applier; }
|
PipeApplier& ServerSession::Applier() { return m_applier; }
|
||||||
ReplyPool& ServerSession::Replies() { return m_replies; }
|
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() {
|
Transport::Doorbell& ServerSession::ConsumerDoorbell() {
|
||||||
MGP5_C0_STUB("ServerSession::ConsumerDoorbell");
|
if (m_transport == nullptr) {
|
||||||
}
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ServerSession::ConsumerDoorbell\"} - no "
|
||||||
Transport::Doorbell& ServerSession::ProducerDoorbell() {
|
"transport; Accept() has not run");
|
||||||
MGP5_C0_STUB("ServerSession::ProducerDoorbell");
|
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
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
|
|||||||
@@ -12,13 +12,32 @@
|
|||||||
// The four segment sizes are already pinned by ProtocolSmokeTest.cpp:72 and are not up for
|
// 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.
|
// 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
|
// 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
|
// 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
|
// (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
|
// 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
|
// 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.
|
// 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
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
@@ -26,8 +45,12 @@
|
|||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
#include "../Transport/Doorbell.h"
|
#include "../Transport/Doorbell.h"
|
||||||
|
#include "../Transport/EventRing.h"
|
||||||
#include "../Transport/ITransport.h"
|
#include "../Transport/ITransport.h"
|
||||||
|
#include "../Transport/ReplySlot.h"
|
||||||
#include "../Transport/Ring.h"
|
#include "../Transport/Ring.h"
|
||||||
|
#include "../Transport/RoleMemory.h"
|
||||||
|
#include "../Transport/SessionRings.h"
|
||||||
#include "../Wire/PipeWireCodec.h"
|
#include "../Wire/PipeWireCodec.h"
|
||||||
#include "PipeApplier.h"
|
#include "PipeApplier.h"
|
||||||
|
|
||||||
@@ -37,6 +60,8 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
public:
|
public:
|
||||||
static ServerSession* Active();
|
static ServerSession* Active();
|
||||||
|
|
||||||
|
~ServerSession();
|
||||||
|
|
||||||
// Maps the four segments, answers Hello with Welcome, and publishes the first
|
// 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
|
// CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is
|
||||||
// decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable)
|
// 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).
|
// cache line otherwise burns a big core for a whole frame on a phone).
|
||||||
Transport::Doorbell& ProducerDoorbell();
|
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:
|
private:
|
||||||
Transport::RingConsumer m_commands;
|
Transport::RingConsumer m_commands;
|
||||||
Wire::SegmentTable m_segments;
|
Wire::SegmentTable m_segments;
|
||||||
PipeApplier m_applier;
|
PipeApplier m_applier;
|
||||||
ReplyPool m_replies;
|
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
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
|
|||||||
Reference in New Issue
Block a user