[Merge] (MGPipe, P5): integrate package s1

# Conflicts:
#	MobileGL/MG_Remote/CapsCodec.cpp
This commit is contained in:
2026-09-11 15:57:13 -04:00
17 changed files with 4180 additions and 86 deletions
+8
View File
@@ -34,6 +34,8 @@
#include "CapsCodec.h"
#include "Protocol/mg_protocol_base.h"
#include <MGGitHash.h>
#include <MG_Util/Debug/Log.h>
@@ -486,6 +488,12 @@ namespace MobileGL::MG_Remote {
hash = FnvU64(hash, kFormatCapabilitiesCodecVersion);
hash = FnvU64(hash, kRendererInfoCodecVersion);
hash = FnvU64(hash, static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount));
// The declared protocol ABI, carried over from s1's version of this function at
// integration (ID-33). The sizeofs above catch a struct that changed shape; this
// catches a peer that changed the PROTOCOL while every struct stayed the same size,
// which is the one break the rest of the mix is blind to.
hash = FnvU64(hash,
static_cast<Uint64>(MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR)));
hash = FnvBytes(hash, GIT_COMMIT_HASH_SHORT, std::strlen(GIT_COMMIT_HASH_SHORT));
return hash;
}
+422 -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,398 @@ 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();
}
// Guarded the same way ServerSession's helpers are: MG_Config::Ipc only exists
// behind MOBILEGL_BUILD_DISAGGREGATED (Config.h), and this file is only compiled
// there today - but the guard is what keeps that true if the source list ever
// changes, and an unguarded read would be a compile error nobody could read.
Uint32 SpinUsFromConfig() {
#if MOBILEGL_BUILD_DISAGGREGATED
return MG_Config::Ipc.SpinUs;
#else
return Transport::kDefaultSpinUs;
#endif
}
Bool VerbBarrierFromConfig() {
#if MOBILEGL_BUILD_DISAGGREGATED
return MG_Config::Ipc.VerbBarrier != 0;
#else
return true;
#endif
}
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);
// msg_as_Welcome() IS PART OF THE GUARD - flatbuffers' Verifier::VerifyTable is
// `return !table || table->Verify(*this)`, so a NULL union member verifies while
// msg_type() still reports Welcome. See ServerSession::Accept for the same guard.
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Welcome ||
envelope->msg_as_Welcome() == nullptr) {
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);
// NO STAGE RING. SEG_STAGE is package w1's encoder-local LINEAR ALLOCATOR:
// a staged byte run carries no RingRecordHeader, nothing consumes SEG_STAGE,
// and the allocator reclaims on retiredSeq. A RingProducer over
// RingCursorSet::Stage would publish stageHead with nothing advancing the
// two tails, so FreeBytes() would fall to zero the first time the head
// lapped the capacity and never recover - a guaranteed hang. See
// RingControl's stage triple in Ring.h.
if (!m_cmd.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_clientTransport->PeerDoorbell(),
&m_clientTransport->SelfDoorbell(), SpinUsFromConfig());
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 = VerbBarrierFromConfig();
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.StageBytes()});
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)});
// nullptr for the stage producer, and that is the honest value: c0's
// signature predates w1's ruling that SEG_STAGE is a linear allocator, and
// the encoder reaches its bytes through the SegmentTable above. Handing it
// a live RingProducer over a cursor triple nobody consumes would be the
// half-wired shape this session exists not to have.
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &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. FIVE of them are
// reached AFTER ServerSession::Accept has already returned OK, so tearing down
// only the client half is not enough and gets three things wrong at once: the
// server keeps its four mappings and stays m_accepted, so Accept's own guard
// refuses every later Start and the process can never open a session again; the
// process-wide segment resolver stays installed; and resetting m_serverTransport
// destroys a transport that ServerSession::m_transport and its two Doorbell*
// still point at. The server closes FIRST, in the same order the started path
// gets right, and only then do the transports go.
m_producer.Detach();
m_shm.Close();
Server::ServerSessionInstance().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) {
// The PRODUCER's own last-published seq, not RingControl::submittedSeq. Ring.h:72-77
// permits submittedSeq to be published lazily and Ring.h:243 encourages batching
// the publish, so the shared watermark is allowed to lag the emitter - and a drain
// that waited for `appliedSeq >= submittedSeq` would then under-wait and free an
// emitter-owned var-tail while a record still names it. With the verb barrier armed
// the two are equal; under MOBILEGL_IPC_VERB_BARRIER=0, R-1's negative control that
// the phase has to run once, they are not.
const Uint64 submitted = m_producer.LastPublishedSeq();
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_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 +433,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
+71 -1
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,66 @@ 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::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
@@ -532,7 +532,8 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_BUILDFINGERPRINT = 8,
VT_BACKENDTYPE = 10,
VT_PID = 12,
VT_CONFIGBLOB = 14
VT_CONFIGBLOB = 14,
VT_ABIFINGERPRINT = 16
};
uint32_t abiMajor() const {
return GetField<uint32_t>(VT_ABIMAJOR, 0);
@@ -552,6 +553,9 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const ::flatbuffers::Vector<uint8_t> *configBlob() const {
return GetPointer<const ::flatbuffers::Vector<uint8_t> *>(VT_CONFIGBLOB);
}
uint64_t abiFingerprint() const {
return GetField<uint64_t>(VT_ABIFINGERPRINT, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -563,6 +567,7 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyField<uint32_t>(verifier, VT_PID, 4) &&
VerifyOffset(verifier, VT_CONFIGBLOB) &&
verifier.VerifyVector(configBlob()) &&
VerifyField<uint64_t>(verifier, VT_ABIFINGERPRINT, 8) &&
verifier.EndTable();
}
};
@@ -589,6 +594,9 @@ struct HelloBuilder {
void add_configBlob(::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> configBlob) {
fbb_.AddOffset(Hello::VT_CONFIGBLOB, configBlob);
}
void add_abiFingerprint(uint64_t abiFingerprint) {
fbb_.AddElement<uint64_t>(Hello::VT_ABIFINGERPRINT, abiFingerprint, 0);
}
explicit HelloBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -607,8 +615,10 @@ inline ::flatbuffers::Offset<Hello> CreateHello(
::flatbuffers::Offset<::flatbuffers::String> buildFingerprint = 0,
uint32_t backendType = 0,
uint32_t pid = 0,
::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> configBlob = 0) {
::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> configBlob = 0,
uint64_t abiFingerprint = 0) {
HelloBuilder builder_(_fbb);
builder_.add_abiFingerprint(abiFingerprint);
builder_.add_configBlob(configBlob);
builder_.add_pid(pid);
builder_.add_backendType(backendType);
@@ -630,7 +640,8 @@ inline ::flatbuffers::Offset<Hello> CreateHelloDirect(
const char *buildFingerprint = nullptr,
uint32_t backendType = 0,
uint32_t pid = 0,
const std::vector<uint8_t> *configBlob = nullptr) {
const std::vector<uint8_t> *configBlob = nullptr,
uint64_t abiFingerprint = 0) {
auto buildFingerprint__ = buildFingerprint ? _fbb.CreateString(buildFingerprint) : 0;
auto configBlob__ = configBlob ? _fbb.CreateVector<uint8_t>(*configBlob) : 0;
return MobileGL::Wire::CreateHello(
@@ -640,7 +651,8 @@ inline ::flatbuffers::Offset<Hello> CreateHelloDirect(
buildFingerprint__,
backendType,
pid,
configBlob__);
configBlob__,
abiFingerprint);
}
struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
@@ -653,7 +665,9 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_CMDRING = 10,
VT_STAGERING = 12,
VT_REPLYPOOL = 14,
VT_EVENTRING = 16
VT_EVENTRING = 16,
VT_BUILDFINGERPRINT = 18,
VT_ABIFINGERPRINT = 20
};
uint32_t abiMajor() const {
return GetField<uint32_t>(VT_ABIMAJOR, 0);
@@ -676,6 +690,12 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const MobileGL::Wire::SegmentRef *eventRing() const {
return GetPointer<const MobileGL::Wire::SegmentRef *>(VT_EVENTRING);
}
const ::flatbuffers::String *buildFingerprint() const {
return GetPointer<const ::flatbuffers::String *>(VT_BUILDFINGERPRINT);
}
uint64_t abiFingerprint() const {
return GetField<uint64_t>(VT_ABIFINGERPRINT, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -690,6 +710,9 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
verifier.VerifyTable(replyPool()) &&
VerifyOffset(verifier, VT_EVENTRING) &&
verifier.VerifyTable(eventRing()) &&
VerifyOffset(verifier, VT_BUILDFINGERPRINT) &&
verifier.VerifyString(buildFingerprint()) &&
VerifyField<uint64_t>(verifier, VT_ABIFINGERPRINT, 8) &&
verifier.EndTable();
}
};
@@ -719,6 +742,12 @@ struct WelcomeBuilder {
void add_eventRing(::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing) {
fbb_.AddOffset(Welcome::VT_EVENTRING, eventRing);
}
void add_buildFingerprint(::flatbuffers::Offset<::flatbuffers::String> buildFingerprint) {
fbb_.AddOffset(Welcome::VT_BUILDFINGERPRINT, buildFingerprint);
}
void add_abiFingerprint(uint64_t abiFingerprint) {
fbb_.AddElement<uint64_t>(Welcome::VT_ABIFINGERPRINT, abiFingerprint, 0);
}
explicit WelcomeBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -738,8 +767,12 @@ inline ::flatbuffers::Offset<Welcome> CreateWelcome(
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> cmdRing = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> stageRing = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> replyPool = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing = 0) {
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing = 0,
::flatbuffers::Offset<::flatbuffers::String> buildFingerprint = 0,
uint64_t abiFingerprint = 0) {
WelcomeBuilder builder_(_fbb);
builder_.add_abiFingerprint(abiFingerprint);
builder_.add_buildFingerprint(buildFingerprint);
builder_.add_eventRing(eventRing);
builder_.add_replyPool(replyPool);
builder_.add_stageRing(stageRing);
@@ -755,6 +788,31 @@ struct Welcome::Traits {
static auto constexpr Create = CreateWelcome;
};
inline ::flatbuffers::Offset<Welcome> CreateWelcomeDirect(
::flatbuffers::FlatBufferBuilder &_fbb,
uint32_t abiMajor = 0,
uint32_t abiMinor = 0,
uint32_t serverPid = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> cmdRing = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> stageRing = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> replyPool = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing = 0,
const char *buildFingerprint = nullptr,
uint64_t abiFingerprint = 0) {
auto buildFingerprint__ = buildFingerprint ? _fbb.CreateString(buildFingerprint) : 0;
return MobileGL::Wire::CreateWelcome(
_fbb,
abiMajor,
abiMinor,
serverPid,
cmdRing,
stageRing,
replyPool,
eventRing,
buildFingerprint__,
abiFingerprint);
}
struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
typedef CapsSnapshotBuilder Builder;
struct Traits;
@@ -764,10 +822,8 @@ struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_FORMATCAPS = 8,
VT_EXTENSIONS = 10,
VT_APIVERSION = 12,
VT_MAXCOMPUTEWORKGROUPCOUNT = 14,
VT_MAXCOMPUTEWORKGROUPSIZE = 16,
VT_TABLESLOTMASK = 18,
VT_PREFERSCPUXFBPRIMITIVEACCOUNTING = 20
VT_CALLMASK = 22,
VT_BACKENDTYPE = 24
};
const ::flatbuffers::Vector<uint8_t> *dynamicParameters() const {
return GetPointer<const ::flatbuffers::Vector<uint8_t> *>(VT_DYNAMICPARAMETERS);
@@ -784,17 +840,11 @@ struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const ::flatbuffers::String *apiVersion() const {
return GetPointer<const ::flatbuffers::String *>(VT_APIVERSION);
}
const ::flatbuffers::Vector<int32_t> *maxComputeWorkGroupCount() const {
return GetPointer<const ::flatbuffers::Vector<int32_t> *>(VT_MAXCOMPUTEWORKGROUPCOUNT);
uint64_t callMask() const {
return GetField<uint64_t>(VT_CALLMASK, 0);
}
const ::flatbuffers::Vector<int32_t> *maxComputeWorkGroupSize() const {
return GetPointer<const ::flatbuffers::Vector<int32_t> *>(VT_MAXCOMPUTEWORKGROUPSIZE);
}
uint64_t tableSlotMask() const {
return GetField<uint64_t>(VT_TABLESLOTMASK, 0);
}
bool prefersCpuXfbPrimitiveAccounting() const {
return GetField<uint8_t>(VT_PREFERSCPUXFBPRIMITIVEACCOUNTING, 0) != 0;
uint32_t backendType() const {
return GetField<uint32_t>(VT_BACKENDTYPE, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
@@ -810,12 +860,8 @@ struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
verifier.VerifyVectorOfStrings(extensions()) &&
VerifyOffset(verifier, VT_APIVERSION) &&
verifier.VerifyString(apiVersion()) &&
VerifyOffset(verifier, VT_MAXCOMPUTEWORKGROUPCOUNT) &&
verifier.VerifyVector(maxComputeWorkGroupCount()) &&
VerifyOffset(verifier, VT_MAXCOMPUTEWORKGROUPSIZE) &&
verifier.VerifyVector(maxComputeWorkGroupSize()) &&
VerifyField<uint64_t>(verifier, VT_TABLESLOTMASK, 8) &&
VerifyField<uint8_t>(verifier, VT_PREFERSCPUXFBPRIMITIVEACCOUNTING, 1) &&
VerifyField<uint64_t>(verifier, VT_CALLMASK, 8) &&
VerifyField<uint32_t>(verifier, VT_BACKENDTYPE, 4) &&
verifier.EndTable();
}
};
@@ -839,17 +885,11 @@ struct CapsSnapshotBuilder {
void add_apiVersion(::flatbuffers::Offset<::flatbuffers::String> apiVersion) {
fbb_.AddOffset(CapsSnapshot::VT_APIVERSION, apiVersion);
}
void add_maxComputeWorkGroupCount(::flatbuffers::Offset<::flatbuffers::Vector<int32_t>> maxComputeWorkGroupCount) {
fbb_.AddOffset(CapsSnapshot::VT_MAXCOMPUTEWORKGROUPCOUNT, maxComputeWorkGroupCount);
void add_callMask(uint64_t callMask) {
fbb_.AddElement<uint64_t>(CapsSnapshot::VT_CALLMASK, callMask, 0);
}
void add_maxComputeWorkGroupSize(::flatbuffers::Offset<::flatbuffers::Vector<int32_t>> maxComputeWorkGroupSize) {
fbb_.AddOffset(CapsSnapshot::VT_MAXCOMPUTEWORKGROUPSIZE, maxComputeWorkGroupSize);
}
void add_tableSlotMask(uint64_t tableSlotMask) {
fbb_.AddElement<uint64_t>(CapsSnapshot::VT_TABLESLOTMASK, tableSlotMask, 0);
}
void add_prefersCpuXfbPrimitiveAccounting(bool prefersCpuXfbPrimitiveAccounting) {
fbb_.AddElement<uint8_t>(CapsSnapshot::VT_PREFERSCPUXFBPRIMITIVEACCOUNTING, static_cast<uint8_t>(prefersCpuXfbPrimitiveAccounting), 0);
void add_backendType(uint32_t backendType) {
fbb_.AddElement<uint32_t>(CapsSnapshot::VT_BACKENDTYPE, backendType, 0);
}
explicit CapsSnapshotBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
@@ -869,20 +909,16 @@ inline ::flatbuffers::Offset<CapsSnapshot> CreateCapsSnapshot(
::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> formatCaps = 0,
::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::flatbuffers::String>>> extensions = 0,
::flatbuffers::Offset<::flatbuffers::String> apiVersion = 0,
::flatbuffers::Offset<::flatbuffers::Vector<int32_t>> maxComputeWorkGroupCount = 0,
::flatbuffers::Offset<::flatbuffers::Vector<int32_t>> maxComputeWorkGroupSize = 0,
uint64_t tableSlotMask = 0,
bool prefersCpuXfbPrimitiveAccounting = false) {
uint64_t callMask = 0,
uint32_t backendType = 0) {
CapsSnapshotBuilder builder_(_fbb);
builder_.add_tableSlotMask(tableSlotMask);
builder_.add_maxComputeWorkGroupSize(maxComputeWorkGroupSize);
builder_.add_maxComputeWorkGroupCount(maxComputeWorkGroupCount);
builder_.add_callMask(callMask);
builder_.add_backendType(backendType);
builder_.add_apiVersion(apiVersion);
builder_.add_extensions(extensions);
builder_.add_formatCaps(formatCaps);
builder_.add_rendererInfo(rendererInfo);
builder_.add_dynamicParameters(dynamicParameters);
builder_.add_prefersCpuXfbPrimitiveAccounting(prefersCpuXfbPrimitiveAccounting);
return builder_.Finish();
}
@@ -898,17 +934,13 @@ inline ::flatbuffers::Offset<CapsSnapshot> CreateCapsSnapshotDirect(
const std::vector<uint8_t> *formatCaps = nullptr,
const std::vector<::flatbuffers::Offset<::flatbuffers::String>> *extensions = nullptr,
const char *apiVersion = nullptr,
const std::vector<int32_t> *maxComputeWorkGroupCount = nullptr,
const std::vector<int32_t> *maxComputeWorkGroupSize = nullptr,
uint64_t tableSlotMask = 0,
bool prefersCpuXfbPrimitiveAccounting = false) {
uint64_t callMask = 0,
uint32_t backendType = 0) {
auto dynamicParameters__ = dynamicParameters ? _fbb.CreateVector<uint8_t>(*dynamicParameters) : 0;
auto rendererInfo__ = rendererInfo ? _fbb.CreateVector<uint8_t>(*rendererInfo) : 0;
auto formatCaps__ = formatCaps ? _fbb.CreateVector<uint8_t>(*formatCaps) : 0;
auto extensions__ = extensions ? _fbb.CreateVector<::flatbuffers::Offset<::flatbuffers::String>>(*extensions) : 0;
auto apiVersion__ = apiVersion ? _fbb.CreateString(apiVersion) : 0;
auto maxComputeWorkGroupCount__ = maxComputeWorkGroupCount ? _fbb.CreateVector<int32_t>(*maxComputeWorkGroupCount) : 0;
auto maxComputeWorkGroupSize__ = maxComputeWorkGroupSize ? _fbb.CreateVector<int32_t>(*maxComputeWorkGroupSize) : 0;
return MobileGL::Wire::CreateCapsSnapshot(
_fbb,
dynamicParameters__,
@@ -916,10 +948,8 @@ inline ::flatbuffers::Offset<CapsSnapshot> CreateCapsSnapshotDirect(
formatCaps__,
extensions__,
apiVersion__,
maxComputeWorkGroupCount__,
maxComputeWorkGroupSize__,
tableSlotMask,
prefersCpuXfbPrimitiveAccounting);
callMask,
backendType);
}
struct DefaultFramebufferInfo FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
+60 -4
View File
@@ -63,6 +63,15 @@ table Hello {
backendType: uint;
pid: uint;
configBlob: [ubyte];
// CONTRACT-P5 table 0, "ABI agreement": MGPCaps has only a COMPOSITIONAL size
// assertion, because DynamicBackendParameters still carries SizeT and GLenum.
// So the two peers assert they were built from the same struct shapes instead
// of rewriting them fixed-width (that is P7's account). This is
// MG_Remote::CapsAbiFingerprint(): sizeof(DynamicBackendParameters),
// sizeof(MGPCaps), sizeof(GLFunctionsTable), the protocol ABI version and the
// build's git stamp, mixed. A mismatch is Fatal{AbiMismatch} and NEVER a
// downgrade - every alternative silently reads one struct as another.
abiFingerprint: ulong;
}
table Welcome {
@@ -73,6 +82,12 @@ table Welcome {
stageRing: SegmentRef;
replyPool: SegmentRef;
eventRing: SegmentRef;
// The server's half of the assertion above. The string is carried beside the
// mixed value only so a mismatch can name both builds in the Fatal line; the
// COMPARISON is on abiFingerprint, which also covers the three sizeofs the
// string cannot.
buildFingerprint: string;
abiFingerprint: ulong;
}
// ---------------------------------------------------------------------------
@@ -83,16 +98,57 @@ table Welcome {
// (plan B appendix A, `get_caps`). The three blobs are byte-for-byte images of
// the corresponding POD structs; they are versioned by structSize-first
// discipline, not by this schema.
// The four fields this table used to end with are RETIRED (CONTRACT-P5 table 0,
// "CapsSnapshot redundancy"):
//
// maxComputeWorkGroupCount / maxComputeWorkGroupSize - they ride inside
// `dynamicParameters` already (BackendObject.h:392-393), and two spellings of
// one number is how the two sides come to disagree about it.
// tableSlotMask - GLFunctionsTable has SIXTY-NINE function-pointer slots
// (BackendObject.h:117-292) and a ulong is 64 bits, so the field could never
// address the table its own comment named; and ARCHITECTURE.md:114 already
// retired "is this table slot null" as the capability probe in favour of
// CallMask, so keeping it would re-introduce exactly what replaced it.
// prefersCpuXfbPrimitiveAccounting - answered by kCapCpuXfbPrimitiveAccounting
// in `callMask` below.
//
// THEY ARE `(deprecated)`, NOT DELETED, AND THAT IS NOT A STYLE CHOICE. In
// FlatBuffers a table field's id IS its vtable slot, and REMOVING a field FREES
// that slot for the next field appended to the table - so plainly deleting these
// four would have handed slots 14 and 16, which used to carry `[int]` vectors
// (4-byte uoffsets), to `callMask` (an 8-byte inline ulong) and `backendType` (a
// 4-byte inline uint). Two peers straddling that edit both still announce
// abiMajor 1, and the ABI fingerprint mixes struct sizes and a git stamp, not the
// schema, so neither the handshake nor the fingerprint could see it: the reader
// would parse a uoffset as a ulong. `(deprecated)` keeps 14/18/20 burned, pushes
// the two new fields to 22/24, generates no accessor for the retired names so
// nothing can read or write them, and costs zero bytes on the wire. The
// alternative - bumping MOBILEGL_PROTOCOL_ABI_MAJOR - is a real break for a
// change that does not need to be one.
table CapsSnapshot {
dynamicParameters: [ubyte];
rendererInfo: [ubyte];
formatCaps: [ubyte];
extensions: [string];
apiVersion: string;
maxComputeWorkGroupCount: [int]; // 3 entries
maxComputeWorkGroupSize: [int]; // 3 entries
tableSlotMask: ulong; // which GLFunctionsTable slots the peer registered
prefersCpuXfbPrimitiveAccounting: bool;
maxComputeWorkGroupCount: [int] (deprecated);
maxComputeWorkGroupSize: [int] (deprecated);
tableSlotMask: ulong (deprecated);
prefersCpuXfbPrimitiveAccounting: bool (deprecated);
// MGPCaps::CallMask (MGPipeTypes.h:127). Bits 0..8 are MGPCapBit; bits 32..47
// are the CONSUMER MASK - bit (32+n) means "the server has a consumer for
// MGPipe subsystem bit n" - and MG_Remote/CapsCodec.h holds the four constexprs
// that are the only legal way to fold and test them. It needs a carrier of its
// own because `dynamicParameters` is the image of DynamicBackendParameters,
// which CallMask is not a member of; and without a carrier R-8's rule that the
// client's liveness gates read the caps mirror has nothing to read.
callMask: ulong;
// The SERVER's backend type, for CapsMirror::Backend(). Hello.backendType is
// the CLIENT's request; this is the answer, and the frontend branches that
// switch on it (GL_Framebuffer.cpp:47, GL_Texture.cpp:6536, CompileEnv.cpp:122)
// take a wrong arm rather than fail on a value they do not know, so it may not
// be guessed from the renderer string.
backendType: uint;
}
table DefaultFramebufferInfo {
+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; }
+485 -17
View File
@@ -6,45 +6,513 @@
// 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_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)
// THE THREE FLAG-SPACE COLLISIONS, AS TRIPWIRES RATHER THAN AS A COMMENT.
//
// MGPipeCallFlags (MG_Pipe/MGPipe.h:42-54) and RingRecordFlags (Ring.h) are separate spaces
// that overlap, and three bits mean DIFFERENT things in each. Ring.h's enum carries the
// table; these are the assertions that break the build if either enum is renumbered, so the
// collision can never become news again. They live here because this is the nearest .cpp
// that legally sees both headers - nothing under Transport/ may reach MobileGL/Includes.h.
static_assert(static_cast<Uint16>(MG_Pipe::kVarTail) == Transport::kRecPad,
"MGPipeCallFlags::kVarTail and kRecPad share bit 2: an encoder that copies call "
"flags into RingRecordHeader::flags makes every var-tail record read as a wrap "
"filler. RingConsumer::Pop requires kind == kRingPadRecordKind as well, which is "
"what keeps that from eating the record - do not relax it");
static_assert(static_cast<Uint16>(MG_Pipe::kHostSpan) == Transport::kRecBorrowSlot,
"MGPipeCallFlags::kHostSpan and kRecBorrowSlot share bit 3: a host-span record "
"would read as borrowed into the GPU timeline and stop the consumer reclaiming "
"ring bytes behind it. SessionConsumer counts and names every sighting");
static_assert(static_cast<Uint16>(MG_Pipe::kReplySlot) == Transport::kRecVarTail,
"MGPipeCallFlags::kReplySlot and kRecVarTail share bit 4");
static_assert(static_cast<Uint16>(MG_Pipe::kNeedsAck) == Transport::kRecNeedsAck &&
static_cast<Uint16>(MG_Pipe::kHasBlob) == Transport::kRecHasBlob,
"the two bits that DO mean the same thing in both spaces have drifted apart, "
"which is a different and worse problem than the three that collide");
ServerSession* ServerSession::Active() { return nullptr; }
namespace {
MobileGLResult ServerSession::Accept(Transport::ITransport&) { MGP5_C0_STUB("ServerSession::Accept"); }
// 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 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();
}
// MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB, actually applied.
//
// The first version of this file called this only `if (m_sizes.CmdBytes == 0)`, and
// SessionSegmentSizes has a default member initialiser of 8 MiB, so the condition was
// never true and the whole function was dead: ConfigLoader parsed both knobs, echoed
// them into the config line, and the session mapped 8/32 MiB regardless. Config.h's
// own comment four lines above the declaration is the statement of that bug - "an
// environment variable that nothing parses is indistinguishable from one that is
// parsed and ignored" - and a knob that REPORTS a value it does not use is worse,
// because it makes every measurement taken with it a lie.
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.CmdRingBytes = 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;
// THERE IS NO DERIVATION OF CallMask, BY RULING. See ServerSession.h's block on
// SetCapabilityBits / SetConsumedSubsystems for why the one that used to be here was
// the phase's marquee defect committed from the server's side.
[[noreturn]] void FatalUnsetCallMask(Bool capBitsSet, Bool consumedSet) {
MGLOG_F("MGPipe: Fatal{UnsetCallMask} - %s%s%s was never set on this ServerSession, "
"and there is no default: a guessed consumer mask makes the client's R-8 "
"liveness gates answer from a server-side fact the server never stated. The "
"client would stop emitting whole record families, clear its dirty flags on "
"acceptance anyway, and the lane would go green with the uploads lost "
"(ID-39, reflected). Call SetConsumedSubsystems() and SetCapabilityBits() "
"before Accept(); SetCapabilityBits(0) is a legitimate explicit answer",
capBitsSet ? "" : "SetCapabilityBits",
(!capBitsSet && !consumedSet) ? " and " : "",
consumedSet ? "" : "SetConsumedSubsystems");
std::abort();
}
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;
m_sizesSet = true;
}
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;
}
Bool ServerSession::CallMaskIsSet() const { return m_capBitsSet && m_consumedSet; }
Uint64 ServerSession::CallMask() const {
if (!CallMaskIsSet()) {
FatalUnsetCallMask(m_capBitsSet, m_consumedSet);
}
return m_capBits | MGCapsConsumerBits(m_consumedSubsystems);
}
Bool ServerSession::Accepted() const { return m_accepted; }
MobileGLResult ServerSession::Accept(Transport::ITransport& transport) {
if (m_accepted) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_transport = &transport;
// MOBILEGL_IPC_RING_MB / _STAGE_MB unless SetSegmentSizes overrode them. Unconditional
// on purpose - see SizesFromConfig.
if (!m_sizesSet) {
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);
// msg_as_Hello() IS PART OF THE GUARD, not a consequence of it. FlatBuffers'
// Verifier::VerifyTable is `return !table || table->Verify(*this)`, so a NULL union
// member passes verification: a 24-byte frame verifies, carries the identifier,
// reports msg_type() == Hello and returns nullptr from msg_as_Hello(). A malformed
// frame has to be refused, never dereferenced.
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Hello ||
envelope->msg_as_Hello() == nullptr) {
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()) {
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()) {
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) {
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.StageBytes()});
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");
if (!CallMaskIsSet()) {
// Not fatal HERE, because a session with no backend legitimately publishes no
// snapshot at all and the mask is only needed by one. It becomes
// Fatal{UnsetCallMask} the moment PublishCapsSnapshot asks for it, which is the
// first thing that would put a guess on the wire.
MGLOG_W("MG_Remote server: accepted with no CallMask - SetConsumedSubsystems() and/or "
"SetCapabilityBits() were never called. There is no default and there will be "
"no guess: the first CapsSnapshot will abort instead");
}
// ---- 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::PublishEvents() {
if (!m_accepted) {
return;
}
// Publish, THEN ring - the same order as the forward direction, and the session picks
// the bell so that no caller can pair the right ring with the wrong flag.
m_events.Ring().Publish();
m_consumer.NotifyClient();
}
// Both of these advance AND ring, through SessionConsumer. The free functions in namespace
// Watermark do not ring: a client parked in WaitForPresentAck(kWaitForever) needs the pair.
void ServerSession::AdvanceCompletedFrame(Uint64 serial) {
if (!m_accepted) {
return;
}
m_consumer.CompleteFrame(serial);
}
void ServerSession::ReturnPresentCredit(Uint64 serial) {
if (!m_accepted) {
return;
}
m_consumer.ReturnPresentCredit(serial);
}
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
+126 -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,111 @@ 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. BOTH ARE MANDATORY AND NEITHER HAS A DEFAULT.
//
// The first version of this file derived a default for each. That was the phase's
// marquee defect committed from the server's side: the consumer default read
// MGPipeGetResourceOps(), a PROCESS-WIDE global (PipeApply.cpp), so under inproc the
// server answered with whatever the client half of the same process had registered,
// and under spawn it collapsed to P2's 0x7f. Either way CapsMirror::ServerConsumes
// then answers a client-side liveness gate with a guess: the client stops emitting
// five P4a families, CLEARS ITS DIRTY FLAGS ON ACCEPTANCE ANYWAY, and the lane goes
// green with the uploads lost - ID-39's 66 lost uploads, reflected. R-8's whole point
// is that a client-side gate may never be answered by a server-side fact; a
// server-side gate answered by a PROCESS-wide fact is the same defect one level down.
//
// So there is no derivation at all. An unset mask is a programming error and
// CallMask() is a named Fatal on one - loud at the first snapshot instead of silent
// for a phase. Flagging it in a report was not a mechanism; this is.
//
// BITS 0..8, THE MGPCapBit FEATURE BITS. What each one answers belongs to the package
// that owns the question (kCapTimerQuery to the query family, kCapResidentSubData to
// b1, and so on). `SetCapabilityBits(0)` is a legitimate and explicit answer - "this
// server offers no optional capability" - and is the right call while those packages
// land. kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes must stay 0 for the whole of
// P5 by ruling (CONTRACT-P5 table 0): they are the only two things that ask for an
// MGHostSpan, and 0 is what keeps every one of them out of the first IPC frame.
void SetCapabilityBits(Uint64 capBits);
// BITS 32..47, THE CONSUMER MASK (R-8 / C-4): which MGPipe subsystems this server has
// a consumer for. v1 owns the answer - it owns the apply thread and knows what its
// backend took over. Publishing a bit the server does not consume is the failure
// above; withholding one the server does consume merely leaves the legacy pull path
// running, which is the safe direction.
void SetConsumedSubsystems(Uint64 subsystemMask);
// False until BOTH setters have been called. A caller that can handle the absence
// asks this; PublishCapsSnapshot and CallMask abort on it.
Bool CallMaskIsSet() const;
// What PublishCapsSnapshot puts on the wire: capBits | MGCapsConsumerBits(subsystems).
// Fatal{UnsetCallMask} if either half was never set.
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();
// Publish everything reserved on SEG_EVENT and ring the client. USE THIS rather than
// EventRingProducer::PublishAndNotify, which takes a bell and a park flag from its
// caller and therefore compiles for every wrong pairing; the session is the thing
// that knows which bell belongs to the client.
void PublishEvents();
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_sizesSet = 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
+205
View File
@@ -0,0 +1,205 @@
// MobileGL - MobileGL/MG_Remote/Transport/EventRing.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SEG_EVENT: the server -> client reverse channel. Owner: package s1.
//
// WHAT P5 OWES HERE AND NOTHING MORE (BRIEF R-12, and s1's brief): it must be
// able to CARRY OnBufferWriteback, OnGpuWritten and OnSurfaceChanged. The other
// seven MGPipeCallbacks members are off P5's reduced path, and THE OVERFLOW
// POLICY IS P9's - what is here is the mechanism (a full ring latches
// eventRingFull, a lossy post that is dropped counts in eventDropped) and not a
// policy that decides between them.
//
// IT IS A SECOND RingControl, NOT A THIRD CURSOR SET. RingControl carries two
// cursor triples (SEG_CMD and SEG_STAGE) and adding a third would resize the
// shared page that Ring.h static_asserts at exactly 4096 bytes. So SEG_EVENT
// gets its OWN control page at its own head and drives it with the Cmd cursor
// set - the same RingProducer/RingConsumer code, in the opposite direction. The
// two EVENT FLAGS still live in the SEG_CMD page, because that is where Ring.h
// declares them and where the client's own waits already look.
//
// THE PAYLOAD SHAPES ARE FIXED-WIDTH AND LIVE HERE, not in MG_Pipe's headers.
// Nothing under Transport/ may reach MobileGL/Includes.h (WireLog.h:9-24, and
// the purity gate's `wire-header` probe), so MGPipeHandle / MGPRange /
// MGPSurfaceInfo cannot be named in this file. The wire shapes below mirror them
// field for field and the SESSIONS assert the two agree, which is the same
// discipline the rest of the wire is held to: a wire struct is fixed-width, and
// the conversion happens where the frontend types are legal.
#pragma once
#include "Doorbell.h"
#include "Ring.h"
#include <cstdint>
#include <cstring>
namespace MobileGL::MG_Remote::Transport {
// Record kinds on SEG_EVENT. 0 is kRingPadRecordKind and can never be an
// event, which is why the list starts at 1.
enum EventKind : std::uint16_t {
kEventNone = 0,
kEventBufferWriteback = 1, // MGPipeCallbacks::OnBufferWriteback
kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten
kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged
};
// The 8-byte {slot, gen} pair, mirrored (MGPipeHandles.h:54-65).
struct EventHandle {
std::uint32_t Slot;
std::uint32_t Gen;
};
static_assert(sizeof(EventHandle) == 8, "the handle is the 8-byte {slot, gen} pair");
// MGPRange, mirrored (MGPipeTypes.h:63-67).
struct EventRange {
std::uint64_t Offset;
std::uint64_t Size;
};
static_assert(sizeof(EventRange) == 16, "MGPRange is 16 bytes on the wire");
// OnBufferWriteback(res, offset, MGPBlobRef bytes). The bytes follow this
// head INSIDE THE RECORD: the blobref the client hands the frontend names
// SEG_EVENT and the in-segment offset of those inline bytes, which is what
// makes "the destination is the client's shadow" (contract table 1 row 22)
// reachable without a second segment. `Size` is therefore the record's own
// tail length and is cross-checked against it.
struct EventBufferWritebackHead {
EventHandle Resource;
std::uint64_t Offset; // destination offset inside the resource
std::uint64_t Size; // inline byte count that follows
};
static_assert(sizeof(EventBufferWritebackHead) == 24, "wire shape");
// OnGpuWritten(res, rangeCount, ranges). EventRange[RangeCount] follows.
struct EventGpuWrittenHead {
EventHandle Resource;
std::uint32_t RangeCount;
std::uint32_t Pad0;
};
static_assert(sizeof(EventGpuWrittenHead) == 16, "wire shape");
// OnSurfaceChanged(const MGPSurfaceInfo*), mirrored (MGPipeTypes.h:1394-1401).
struct EventSurfaceChangedHead {
std::uint32_t Width;
std::uint32_t Height;
std::uint32_t InternalFormat;
std::uint16_t Samples;
std::uint16_t Layers;
std::uint8_t IsDefault;
std::uint8_t Pad0[7];
};
static_assert(sizeof(EventSurfaceChangedHead) == 24, "MGPSurfaceInfo is 24 bytes on the wire");
// The server's end. One producer: the apply thread, by construction.
class EventRingProducer {
public:
EventRingProducer() = default;
// `eventControl` is SEG_EVENT's own control page; `cmdControl` is the
// SEG_CMD page, because Ring.h declares eventRingFull / eventDropped
// there and the client's waits already look at it.
EventRingProducer(RingControl* eventControl, RingControl* cmdControl, void* base,
std::uint64_t capacityBytes)
: m_cmdControl(cmdControl),
m_producer(eventControl, base, capacityBytes, RingCursorSet::Cmd) {}
bool Valid() const { return m_producer.Valid() && m_cmdControl != nullptr; }
// Reserves one event record. nullptr means the ring is full: the caller
// decides, and the two flags are how it says which decision it took.
// THIS FUNCTION DOES NOT DECIDE - that is P9's.
void* Reserve(EventKind kind, std::uint64_t payloadBytes) {
void* slot = m_producer.Reserve(static_cast<std::uint16_t>(kind), kRecNone, payloadBytes);
if (slot == nullptr && m_cmdControl != nullptr) {
// "SEG_EVENT full, server stopped applying" - Ring.h:123. Latched
// here, cleared by the consumer once it has drained.
m_cmdControl->eventRingFull.store(1, std::memory_order_release);
}
return slot;
}
// For a LOSSY event the caller could not place. Lossless events must
// never call this; they wait for the client to drain instead.
void CountDrop() {
if (m_cmdControl != nullptr) {
m_cmdControl->eventDropped.fetch_add(1, std::memory_order_relaxed);
}
}
// Publish, THEN ring - the same order as the forward direction, and for
// the same reason (Doorbell.h:186-193: the fence only orders what
// precedes it, so ringing first reopens the lost-wakeup window).
void PublishAndNotify(Doorbell& clientBell, std::atomic<std::uint32_t>& producerParked) {
m_producer.Publish();
NotifyIfParked(clientBell, producerParked);
}
RingProducer& Ring() { return m_producer; }
private:
RingControl* m_cmdControl = nullptr;
RingProducer m_producer;
};
// The client's end. One consumer: the GL thread, which drains between verbs.
class EventRingConsumer {
public:
EventRingConsumer() = default;
EventRingConsumer(RingControl* eventControl, RingControl* cmdControl, void* base,
std::uint64_t capacityBytes, const void* segmentBase)
: m_cmdControl(cmdControl),
m_consumer(eventControl, base, capacityBytes, RingCursorSet::Cmd),
m_segmentBase(static_cast<const std::uint8_t*>(segmentBase)) {}
bool Valid() const { return m_consumer.Valid() && m_cmdControl != nullptr; }
bool Pop(RingRecordView& out, bool* outCorrupt = nullptr) {
return m_consumer.Pop(out, outCorrupt);
}
// Release the bytes and clear the full latch. Only after the caller has
// finished with every payload pointer it popped: a writeback's bytes live
// in the ring itself, so retiring early is the R-11 violation one level
// down.
void Drained() {
m_consumer.PublishRetired();
if (m_cmdControl != nullptr) {
m_cmdControl->eventRingFull.store(0, std::memory_order_release);
}
}
// Byte offset of `payload` inside SEG_EVENT, which is what an
// OnBufferWriteback MGPBlobRef must carry (Seg = kSegEvent, Offset =
// this, Size = the head's Size). Never a host address - R-2's rule B.
std::uint64_t OffsetInSegment(const void* payload) const {
return static_cast<std::uint64_t>(static_cast<const std::uint8_t*>(payload) -
m_segmentBase);
}
std::uint64_t DroppedEvents() const {
return m_cmdControl == nullptr
? 0
: m_cmdControl->eventDropped.load(std::memory_order_relaxed);
}
bool RingIsFull() const {
return m_cmdControl != nullptr &&
m_cmdControl->eventRingFull.load(std::memory_order_acquire) != 0;
}
RingConsumer& Ring() { return m_consumer; }
private:
RingControl* m_cmdControl = nullptr;
RingConsumer m_consumer;
const std::uint8_t* m_segmentBase = nullptr;
};
} // namespace MobileGL::MG_Remote::Transport
+274
View File
@@ -0,0 +1,274 @@
// MobileGL - MobileGL/MG_Remote/Transport/ReplySlot.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SEG_REPLY: the slot pool the server writes a kReplySlot answer into, and the
// client reads it back out of. Owner: package s1.
//
// THE ID IS THE RECORD SEQUENCE NUMBER (P5 R-3). There is no second id space and
// no allocator: the ten kReplySlot calls carry no MGPReplySlot in their payloads
// (that is why the id has to be DERIVED rather than carried), the wire has no
// per-record seq field (ARCHITECTURE.md:124), so the record's ordinal IS its
// reply-slot id. The slot is addressed `seq % slotCount` and the server STAMPS
// THE SEQ BACK INTO THE SLOT HEADER, which is what makes a wrong-slot read
// detectable rather than merely plausible. Seq is 1-based; 0 means "no record".
//
// THE SLOT HEADER IS CONTRACT-P5 TABLE 0's ROW, verbatim:
// { Uint64 Seq; Int32 Status; Uint32 Size; } // 16 bytes, then the payload
// Status: 0 = OK, 1 = DECLINED, 2 = ERROR
//
// DECLINED IS A REAL ANSWER, NOT A FAILURE. It is how MapPersistent says nullptr
// (R-6) and how the four Bool acceptance entry points - ResourceCreate,
// ResourceRespecify, ResourceSubData, SetTextureParams - say false (R-5). A
// client that folds DECLINED into "the call failed" re-creates ID-39's 66 lost
// uploads from the other side, and a client that folds it into OK accepts a
// pointer the server never handed out.
//
// A REPLY LARGER THAN ONE SLOT IS FATAL, NOT CHUNKED. P5's only large answer is
// ReadPixels, and the client knows its size before it emits the record, so an
// overflow means the two sides disagree about the frame rather than that the
// pool is too small. Chunking is P8's; growing the pool is an operator's.
//
// ORDERING. The client only looks at a slot after it has seen
// RingControl::appliedSeq >= its own seq with an ACQUIRE load, and the server
// advances appliedSeq with a RELEASE store AFTER posting the reply
// (PipeApplier::ApplyOne's order: decode -> stamp -> apply -> post -> advance).
// That pair is what publishes the slot's bytes; the fences below are the
// belt-and-braces for a caller - a unit test, or P9's async pool - that reads a
// slot without going through appliedSeq first.
#pragma once
#include "WireLog.h"
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <cstring>
// P9's account, named here rather than left to be rediscovered: the seq stamp
// catches a sequence space drifted by anything that is NOT a multiple of
// slotCount. A drift of exactly 8, 16, ... lands on the same slot with a matching
// stamp and reads as this call's answer. Under R-1's verb barrier the in-flight
// depth is one and a drift cannot open at all; P9 is what removes the barrier,
// and it is what has to widen the stamp (a generation beside the seq) or bound
// the drift some other way.
namespace MobileGL::MG_Remote::Transport {
// CONTRACT-P5 table 0, "reply slot header".
struct ReplySlotHeader {
std::uint64_t Seq; // the record ordinal, stamped back for self-check
std::int32_t Status; // ReplyStatus
std::uint32_t Size; // payload bytes following this header
};
static_assert(sizeof(ReplySlotHeader) == 16, "the reply slot header is 16 bytes on the wire");
static_assert(alignof(ReplySlotHeader) == 8, "the reply slot header must not gain padding");
enum ReplyStatus : std::int32_t {
kReplyStatusOk = 0,
// Not an error. MapPersistent's nullptr and the four Bool acceptance
// returns' `false` both arrive as this.
kReplyStatusDeclined = 1,
kReplyStatusError = 2,
};
// Eight slots of a 8 MiB SEG_REPLY is 1 MiB per answer.
//
// Why eight and not sixty-four: while the verb barrier holds (R-1) the client
// blocks at every verb boundary, so the in-flight depth is exactly ONE and
// every extra slot buys nothing but a smaller maximum answer. The trade is
// the other way round - fewer slots, bigger replies - and P5's only large
// answer is a blocking ReadPixels. 1 MiB covers a 512x512 RGBA8 read. P9,
// which is what makes the pool asynchronous, re-chooses this geometry with
// real depth to size it against.
inline constexpr std::uint32_t kDefaultReplySlotCount = 8;
// Slot 0 exists and is used: seq is 1-based, so seq % slotCount hits slot 0
// on seq == slotCount, not on "no record".
class ReplySlotPool {
public:
ReplySlotPool() = default;
// `base`/`sizeBytes` are SEG_REPLY's mapping. `slotCount` must be a power
// of two - the addressing is a mask, and a non-power-of-two modulus on
// the apply thread is a division in the reply path of every blocking
// call. Anything else leaves Valid() false rather than half-working.
ReplySlotPool(void* base, std::uint64_t sizeBytes, std::uint32_t slotCount) {
if (base == nullptr || slotCount == 0 || (slotCount & (slotCount - 1)) != 0) {
WireLogError("MG_Remote reply pool: rejected, slotCount %u must be a non-zero power "
"of two over a non-null mapping",
static_cast<unsigned>(slotCount));
return;
}
const std::uint64_t slotBytes = sizeBytes / slotCount;
if (slotBytes <= sizeof(ReplySlotHeader)) {
WireLogError("MG_Remote reply pool: rejected, %llu bytes over %u slots leaves no "
"room for a payload past the %llu byte slot header",
static_cast<unsigned long long>(sizeBytes),
static_cast<unsigned>(slotCount),
static_cast<unsigned long long>(sizeof(ReplySlotHeader)));
return;
}
// EVERY SLOT MUST START 8-ALIGNED. The header is written by one thread
// and read by another; the fences below order the payload against the
// stamp, but the stamp's own 8-byte Seq has to be untorn for the
// wrong-slot self-check to mean anything, and that is only true while
// it is naturally aligned. A geometry whose slotBytes is not a
// multiple of 8 puts later slots on odd boundaries, so it is refused
// here rather than left to a future caller to discover.
if ((slotBytes % 8) != 0 ||
(reinterpret_cast<std::uintptr_t>(base) % alignof(ReplySlotHeader)) != 0) {
WireLogError("MG_Remote reply pool: rejected, a %llu byte slot at base alignment "
"%llu would put a slot header on an unaligned address, and the seq "
"stamp the wrong-slot check reads has to be untorn",
static_cast<unsigned long long>(slotBytes),
static_cast<unsigned long long>(
reinterpret_cast<std::uintptr_t>(base) % alignof(ReplySlotHeader)));
return;
}
m_base = static_cast<std::uint8_t*>(base);
m_slots = slotCount;
m_mask = slotCount - 1;
// Truncated to 32 bits deliberately: the header's Size field is
// 32-bit, so a slot no 32-bit count could describe would let a
// legal-looking Size name bytes past the slot.
m_slotBytes = slotBytes > 0xFFFFFFFFull ? 0xFFFFFFFFu
: static_cast<std::uint32_t>(slotBytes);
}
bool Valid() const { return m_base != nullptr; }
std::uint32_t SlotCount() const { return m_slots; }
std::uint32_t SlotBytes() const { return m_slotBytes; }
// What a single answer may carry. The client checks against this BEFORE
// it emits a ReadPixels, which is the whole reason a fixed slot size is
// legitimate rather than a guess.
std::uint32_t MaxReplyBytes() const {
return m_slotBytes == 0 ? 0u
: m_slotBytes - static_cast<std::uint32_t>(sizeof(ReplySlotHeader));
}
// Zeroes every header, so a stale seq from a previous session cannot be
// mistaken for this session's answer. Called on the server side at Accept.
void Clear() {
if (m_base == nullptr) {
return;
}
for (std::uint32_t slot = 0; slot < m_slots; ++slot) {
ReplySlotHeader header{};
std::memcpy(m_base + static_cast<std::uint64_t>(slot) * m_slotBytes, &header,
sizeof(header));
}
}
// Server side. `size` bytes of `bytes` become the answer for `seq`.
// An answer larger than one slot is FATAL, never truncated and never
// chunked - see the file header.
void Post(std::uint64_t seq, std::int32_t status, const void* bytes, std::uint64_t size) {
if (m_base == nullptr) {
WireLogError("MG_Remote reply pool: Post(seq=%llu) on an unconfigured pool",
static_cast<unsigned long long>(seq));
std::abort();
}
if (seq == 0) {
WireLogError("MG_Remote reply pool: seq 0 is \"no record\" and can never name a "
"slot (R-3: seq is 1-based)");
std::abort();
}
if (size > MaxReplyBytes()) {
WireLogError("MG_Remote reply pool: Fatal{ProtocolCorruption} - a %llu byte answer "
"for seq %llu does not fit a %u byte slot (payload cap %u). P5 does "
"not chunk replies: the client knows an answer's size before it emits "
"the record, so this means the two sides disagree about the frame",
static_cast<unsigned long long>(size),
static_cast<unsigned long long>(seq), static_cast<unsigned>(m_slotBytes),
static_cast<unsigned>(MaxReplyBytes()));
std::abort();
}
std::uint8_t* slot = SlotAt(seq);
if (size != 0 && bytes != nullptr) {
std::memcpy(slot + sizeof(ReplySlotHeader), bytes, static_cast<std::size_t>(size));
}
ReplySlotHeader header{};
header.Seq = seq;
header.Status = status;
header.Size = static_cast<std::uint32_t>(size);
// The payload must be visible before the stamp that says it is there.
std::atomic_thread_fence(std::memory_order_release);
std::memcpy(slot, &header, sizeof(header));
}
// Client side. Returns false when the slot does not carry THIS seq - the
// self-check the stamp exists for. `outBytes` may be null for an answer
// with no payload (every DECLINE, and the four Bool acceptances).
//
// A payload larger than the caller's buffer is a caller bug rather than a
// wire fault (the caller sized it from the call it made), so it returns
// false with *outSize set to what was there, the ReceiveFrame shape.
bool Read(std::uint64_t seq, void* outBytes, std::uint64_t outCapacity,
std::int32_t* outStatus, std::uint64_t* outSize) const {
if (outStatus != nullptr) {
*outStatus = kReplyStatusError;
}
if (outSize != nullptr) {
*outSize = 0;
}
if (m_base == nullptr || seq == 0) {
return false;
}
const std::uint8_t* slot = SlotAt(seq);
ReplySlotHeader header{};
std::memcpy(&header, slot, sizeof(header));
std::atomic_thread_fence(std::memory_order_acquire);
if (header.Seq != seq) {
// Not "retry": under the verb barrier the answer is already
// there by the time appliedSeq passed this record, so a stamp
// that disagrees is a drifted sequence space (R-9's kRecPad
// rule) or a wrong-slot read, and both are faults.
WireLogError("MG_Remote reply pool: slot %llu carries seq %llu, not %llu - the two "
"sides' sequence spaces have drifted (a counted kRecPad, R-9) or the "
"addressing disagrees",
static_cast<unsigned long long>(seq & m_mask),
static_cast<unsigned long long>(header.Seq),
static_cast<unsigned long long>(seq));
return false;
}
if (header.Size > MaxReplyBytes()) {
WireLogError("MG_Remote reply pool: slot for seq %llu declares %u payload bytes in "
"a %u byte slot",
static_cast<unsigned long long>(seq), header.Size,
static_cast<unsigned>(m_slotBytes));
return false;
}
if (outStatus != nullptr) {
*outStatus = header.Status;
}
if (outSize != nullptr) {
*outSize = header.Size;
}
if (header.Size == 0) {
return true;
}
if (outBytes == nullptr || outCapacity < header.Size) {
return false;
}
std::memcpy(outBytes, slot + sizeof(ReplySlotHeader), header.Size);
return true;
}
private:
std::uint8_t* SlotAt(std::uint64_t seq) const {
return m_base + (seq & m_mask) * static_cast<std::uint64_t>(m_slotBytes);
}
std::uint8_t* m_base = nullptr;
std::uint32_t m_slots = 0;
std::uint32_t m_mask = 0;
std::uint32_t m_slotBytes = 0;
};
} // namespace MobileGL::MG_Remote::Transport
+338 -1
View File
@@ -8,8 +8,11 @@
#include "Ring.h"
#include "SessionRings.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Remote::Transport {
@@ -257,7 +260,16 @@ namespace MobileGL::MG_Remote::Transport {
return false;
}
if ((header.flags & kRecPad) != 0) {
// BOTH, not just the flag. kRecPad (1<<2) is the same bit as
// MGPipeCallFlags::kVarTail, so an encoder that copied a call's flags
// into this framing field verbatim would have every var-tail record
// skipped HERE, silently, with the record lost and nothing logged on
// either side. A genuine filler is always kind kRingPadRecordKind -
// Reserve writes it two dozen lines above - and a call record always
// carries a real opcode, because the catalogue starts at 1. Requiring
// the pair costs one comparison and turns that collision from a lost
// record into a record the decoder gets and can reject by name.
if ((header.flags & kRecPad) != 0 && header.kind == kRingPadRecordKind) {
m_localTail += size;
continue;
}
@@ -303,4 +315,329 @@ namespace MobileGL::MG_Remote::Transport {
}
}
// =======================================================================
// P5: the five watermarks, the two session endpoints, and the ABI mixer.
// =======================================================================
std::uint64_t LargestPowerOfTwoAtMost(std::uint64_t bytes) {
if (bytes == 0) {
return 0;
}
std::uint64_t value = 1;
while (value <= (bytes >> 1)) {
value <<= 1;
}
return value;
}
std::uint64_t SegmentBytesForRing(std::uint64_t ringBytes) {
const std::uint64_t ring = LargestPowerOfTwoAtMost(ringBytes);
if (ring < kMinRingCapacity || ring > kMaxRingCapacity) {
return 0;
}
return ring + sizeof(RingControl);
}
std::uint64_t RingCapacityForSegment(std::uint64_t segmentBytes) {
if (segmentBytes <= sizeof(RingControl)) {
return 0;
}
const std::uint64_t usable = LargestPowerOfTwoAtMost(segmentBytes - sizeof(RingControl));
return usable < kMinRingCapacity ? 0 : usable;
}
namespace Watermark {
namespace {
// A watermark may be published LATE but never EARLY, and it may never
// move BACKWARDS. Backwards is the half that is mechanically
// detectable from inside, and it is FATAL rather than logged: the
// consumer keeps its own counter, so a shared watermark left behind
// makes every later advance a no-op and every WaitForApplied on
// kWaitForever - the verb barrier and every reply wait - block for
// ever. A hang with one ERROR line in the log is strictly worse than
// an abort at the instruction that caused it, and Ring.h:181-185
// already rules the same way for the cursor invariants on this page.
// "Early" can only be caught at the call site, which is why every
// advance below has exactly one caller and a named unit case.
void AdvanceMonotonic(std::atomic<std::uint64_t>& watermark, std::uint64_t to,
const char* name) {
const std::uint64_t current = watermark.load(std::memory_order_relaxed);
if (to < current) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"watermark\"} %s moved backwards, "
"%llu -> %llu. A waiter that already resumed on the higher value cannot "
"be un-resumed, and every later advance of this watermark would be a "
"no-op, so the verb barrier and every reply wait would block for ever",
name, static_cast<unsigned long long>(current),
static_cast<unsigned long long>(to));
std::abort();
}
if (to == current) {
return;
}
// Release: everything the advance is a statement ABOUT - the
// record that was applied, the staged bytes that were drained,
// the reply that was posted - must be visible to the acquiring
// waiter before the number that says it happened.
watermark.store(to, std::memory_order_release);
}
} // namespace
void AdvanceSubmitted(RingControl& control, std::uint64_t seq) {
AdvanceMonotonic(control.submittedSeq, seq, "submittedSeq");
}
void AdvanceApplied(RingControl& control, std::uint64_t seq) {
AdvanceMonotonic(control.appliedSeq, seq, "appliedSeq");
}
void AdvanceRetired(RingControl& control, std::uint64_t seq) {
// retiredSeq may never overtake appliedSeq: the staging allocator
// reclaims behind it, so a retire ahead of the apply hands live bytes
// back to the producer. Clamped rather than refused, because a
// caller that retires "everything applied" is the normal shape.
const std::uint64_t applied = control.appliedSeq.load(std::memory_order_acquire);
AdvanceMonotonic(control.retiredSeq, seq > applied ? applied : seq, "retiredSeq");
}
void AdvanceCompletedFrame(RingControl& control, std::uint64_t serial) {
AdvanceMonotonic(control.completedFrameSerial, serial, "completedFrameSerial");
}
void AdvancePresentAck(RingControl& control, std::uint64_t serial) {
AdvanceMonotonic(control.presentAckSerial, serial, "presentAckSerial");
}
} // namespace Watermark
// -----------------------------------------------------------------------
// SessionProducer
// -----------------------------------------------------------------------
void SessionProducer::Attach(RingControl* control, RingProducer* cmd, Doorbell* peerBell,
Doorbell* selfBell, std::uint32_t spinUs) {
m_control = control;
m_cmd = cmd;
m_peerBell = peerBell;
m_selfBell = selfBell;
m_spinUs = spinUs;
}
void SessionProducer::Detach() {
m_control = nullptr;
m_cmd = nullptr;
m_peerBell = nullptr;
m_selfBell = nullptr;
m_lastPublishedSeq = 0;
}
void SessionProducer::PublishAndNotify(std::uint64_t submittedSeq) {
if (!Valid()) {
return;
}
// 1. the records themselves. ONLY SEG_CMD: SEG_STAGE is not a ring and
// RingCursorSet::Stage is driven by nobody (Ring.h's stage triple).
m_cmd->Publish();
// Kept locally as well as on the shared page: the shared watermark is
// allowed to lag (Ring.h:72-77), and teardown's drain must not.
//
// Clamped UP, not passed through. A caller that republishes an older
// bound - teardown does exactly that, and so does any batched publisher
// that lost track - is publishing LATE, which R-9 permits; it is not the
// same thing as a watermark moving backwards, which is Fatal. Doing the
// clamp here keeps that distinction at the one boundary where a stale
// argument is legitimate.
if (submittedSeq > m_lastPublishedSeq) {
m_lastPublishedSeq = submittedSeq;
}
// 2. the diagnostic watermark, after the bytes it describes.
Watermark::AdvanceSubmitted(*m_control, m_lastPublishedSeq);
// 3. and only now the bell. Publish-then-ring, never ring-then-publish.
if (m_peerBell != nullptr) {
NotifyIfParked(*m_peerBell, m_control->consumerParked);
}
}
template <class Ready>
SessionWait SessionProducer::Park(Ready&& ready, std::uint32_t timeoutMs) {
if (!Valid() || m_selfBell == nullptr) {
return SessionWait::TimedOut;
}
if (m_selfBell->Wait(m_control->producerParked, ready, m_spinUs, timeoutMs)) {
return SessionWait::Reached;
}
// Wait == false && Dead() is "the session was shut down", and it is the
// only thing that returns from a kWaitForever park. Anything else is the
// deadline.
return m_selfBell->Dead() ? SessionWait::ShutDown : SessionWait::TimedOut;
}
SessionWait SessionProducer::WaitForApplied(std::uint64_t seq, std::uint32_t timeoutMs) {
if (!Valid()) {
return SessionWait::TimedOut;
}
RingControl* control = m_control;
return Park([control, seq] { return Watermark::Reached(control->appliedSeq, seq); },
timeoutMs);
}
SessionWait SessionProducer::WaitForPresentAck(std::uint64_t serial, std::uint32_t timeoutMs) {
if (!Valid()) {
return SessionWait::TimedOut;
}
RingControl* control = m_control;
return Park([control, serial] { return Watermark::Reached(control->presentAckSerial, serial); },
timeoutMs);
}
SessionWait SessionProducer::WaitForCmdSpace(std::uint64_t bytes, std::uint32_t timeoutMs) {
if (!Valid()) {
return SessionWait::TimedOut;
}
RingProducer* cmd = m_cmd;
return Park([cmd, bytes] { return cmd->FreeBytes() >= bytes; }, timeoutMs);
}
// -----------------------------------------------------------------------
// SessionConsumer
// -----------------------------------------------------------------------
void SessionConsumer::Attach(RingControl* control, RingConsumer* cmd, Doorbell* peerBell,
Doorbell* selfBell, std::uint32_t spinUs) {
m_control = control;
m_cmd = cmd;
m_peerBell = peerBell;
m_selfBell = selfBell;
m_spinUs = spinUs;
m_appliedSeq = control == nullptr ? 0 : control->appliedSeq.load(std::memory_order_acquire);
m_retirableCursor = cmd == nullptr ? 0 : cmd->LocalTail();
m_borrowHeld = false;
}
void SessionConsumer::Detach() {
m_control = nullptr;
m_cmd = nullptr;
m_peerBell = nullptr;
m_selfBell = nullptr;
m_retirableCursor = 0;
m_borrowHeld = false;
}
SessionWait SessionConsumer::WaitForWork(std::uint32_t timeoutMs) {
if (!Valid() || m_selfBell == nullptr) {
return SessionWait::TimedOut;
}
RingControl* control = m_control;
RingConsumer* cmd = m_cmd;
const bool woke = m_selfBell->Wait(
control->consumerParked,
[control, cmd] {
return control->cmdHead.load(std::memory_order_acquire) != cmd->LocalTail();
},
m_spinUs, timeoutMs);
if (woke) {
return SessionWait::Reached;
}
return m_selfBell->Dead() ? SessionWait::ShutDown : SessionWait::TimedOut;
}
void SessionConsumer::RetireThrough(std::uint64_t seq) {
if (!Valid()) {
return;
}
Watermark::AdvanceRetired(*m_control, seq);
// PublishRetiredUpTo, NOT PublishRetired: the latter stores m_localTail
// into BOTH tails, i.e. it hands back every byte the consumer has popped
// whether or not a record among them was borrowed into the GPU timeline.
// That would defeat the whole reason the ring carries two tails
// (Ring.h:24-27) and would let the producer overwrite a slot the GPU is
// still reading. m_retirableCursor stops at the first borrowed record.
m_cmd->PublishApplied();
m_cmd->PublishRetiredUpTo(m_retirableCursor);
NotifyClient();
}
void SessionConsumer::NoteBorrowedRecord(std::uint16_t kind, std::uint16_t flags) {
++m_borrowedSeen;
if (m_borrowedSeen == 1) {
MGLOG_E("MG_Remote ring: record kind %u carries kRecBorrowSlot (flags=0x%04X). P5 "
"implements NO borrowed slots, and that bit is also MGPipeCallFlags::"
"kHostSpan, which P5's reduced path is ruled to produce none of either. "
"Nothing past this record will be reclaimed until RetireBorrowedUpTo "
"releases it, so a producer that then wedges on a full ring is THIS line's "
"fault and not the ring's",
static_cast<unsigned>(kind), static_cast<unsigned>(flags));
}
}
void SessionConsumer::RetireBorrowedUpTo(std::uint64_t cursor) {
if (!Valid()) {
return;
}
if (cursor > m_retirableCursor) {
m_retirableCursor = cursor;
// A release that reaches everything popped so far clears the latch;
// anything still unreleased keeps it set, so a second borrow behind
// the first is not skipped.
m_borrowHeld = cursor < m_cmd->LocalTail();
}
m_cmd->PublishRetiredUpTo(m_retirableCursor);
NotifyClient();
}
void SessionConsumer::CompleteFrame(std::uint64_t serial) {
if (!Valid()) {
return;
}
Watermark::AdvanceCompletedFrame(*m_control, serial);
NotifyClient();
}
void SessionConsumer::ReturnPresentCredit(std::uint64_t serial) {
if (!Valid()) {
return;
}
Watermark::AdvancePresentAck(*m_control, serial);
NotifyClient();
}
void SessionConsumer::NotifyClient() {
if (m_control != nullptr && m_peerBell != nullptr) {
NotifyIfParked(*m_peerBell, m_control->producerParked);
}
}
// -----------------------------------------------------------------------
// The ABI fingerprint's mixer
// -----------------------------------------------------------------------
std::uint64_t MixAbiFingerprint(std::uint64_t dynamicParamsSize, std::uint64_t capsSize,
std::uint64_t functionTableSize, std::uint32_t abiVersion,
const char* buildStamp) {
// FNV-1a over the four numbers and the stamp. Not a hash with any
// security property and not meant to be one: it has to (a) change when
// ANY input changes and (b) be computable identically in two processes
// built from one source tree, which rules out anything seeded at runtime.
std::uint64_t hash = 1469598103934665603ull;
const auto mix = [&hash](std::uint64_t value) {
for (int byte = 0; byte < 8; ++byte) {
hash ^= static_cast<std::uint64_t>((value >> (byte * 8)) & 0xFF);
hash *= 1099511628211ull;
}
};
mix(dynamicParamsSize);
mix(capsSize);
mix(functionTableSize);
mix(abiVersion);
if (buildStamp != nullptr) {
for (const char* c = buildStamp; *c != '\0'; ++c) {
hash ^= static_cast<std::uint64_t>(static_cast<unsigned char>(*c));
hash *= 1099511628211ull;
}
}
// 0 is reserved for "not stated": a peer that forgot to fill the field
// must not accidentally agree with one that did.
return hash == 0 ? 1ull : hash;
}
} // namespace MobileGL::MG_Remote::Transport
+51 -1
View File
@@ -103,7 +103,30 @@ namespace MobileGL::MG_Remote::Transport {
alignas(64) std::atomic<std::uint64_t> cmdAppliedTail; // consumer: bytes decoded/copied out
std::atomic<std::uint64_t> cmdRetiredTail; // consumer: borrowed slots released
// ---- SEG_STAGE cursors ----------------------------------------------
// ---- SEG_STAGE cursors ------------------------------------------------
//
// DEAD IN P5, DELIBERATELY, AND NOBODY MAY WIRE THEM UP HALFWAY.
//
// SEG_STAGE is NOT a ring any more. Package w1's encoder owns staging as
// an ENCODER-LOCAL LINEAR ALLOCATOR: a staged byte run carries no
// RingRecordHeader, there is no consumer walking SEG_STAGE, and the
// allocator reclaims on `retiredSeq` - the sequence watermark below -
// rather than on these three cursors. So all three stay ZERO for the
// whole of P5, `RingCursorSet::Stage` has no producer and no consumer,
// and `SessionTest.TheStageCursorTripleStaysDeadAcrossAWholeSession`
// pins that rather than leaving it to be noticed.
//
// They are kept rather than deleted because RingCursorSet, the three
// cursor accessors in Ring.cpp and RingTest's fixture are all written
// against a two-triple page, and P8/P11's shadow and adopt segments are
// the ring-shaped users this triple was reserved for. What is NOT
// acceptable is the middle state: a producer publishing `stageHead` with
// nothing advancing the two tails makes FreeBytes() fall to zero the
// first time the head laps the capacity and never recover, which is a
// guaranteed hang rather than a slow path. Five watermarks already spent
// a whole phase declared-and-written-by-nobody; this is the sixth, and
// it is declared-and-written-by-nobody ON PURPOSE, which is only
// different if it is written down.
alignas(64) std::atomic<std::uint64_t> stageHead;
alignas(64) std::atomic<std::uint64_t> stageAppliedTail;
std::atomic<std::uint64_t> stageRetiredTail;
@@ -140,6 +163,33 @@ namespace MobileGL::MG_Remote::Transport {
};
static_assert(sizeof(RingRecordHeader) == 8, "RecHeader is 8 bytes on the wire");
// THESE ARE RING FLAGS AND THEY ARE NOT MGPipeCallFlags, AND THREE OF THE
// BITS COLLIDE WITH A DIFFERENT MEANING. `MGPipeCallFlags` (MG_Pipe/MGPipe.h:
// 42-54) is a SEPARATE SPACE that happens to overlap this one, and an encoder
// that copies `MGPipeCallFlagsFor(op)` into RingRecordHeader::flags without
// translating puts a call's bits into a framing field:
//
// bit 0 kNeedsAck == kRecNeedsAck same meaning, harmless
// bit 1 kHasBlob == kRecHasBlob same meaning, harmless
// bit 2 kVarTail == kRecPad WORST: a var-tail record would read
// as a WRAP FILLER and be skipped
// silently by Pop, losing the record
// with nothing logged anywhere
// bit 3 kHostSpan == kRecBorrowSlot a host-span record would read as
// borrowed into the GPU timeline, and
// the consumer would stop reclaiming
// ring bytes behind it for ever
// bit 4 kReplySlot == kRecVarTail a blocking call would read as having
// a tail it does not have
// bit 5 kOptional == (unused here)
//
// Translating is the ENCODER's job. Two things on this side make the first
// two of those survivable anyway rather than trusting it: `Pop` requires a
// filler to carry BOTH kRecPad AND kind == kRingPadRecordKind, so a real
// record with bit 2 set is delivered rather than eaten (a call record always
// has a real opcode kind, the catalogue starts at 1); and SessionConsumer
// counts and NAMES every kRecBorrowSlot it sees, because P5 produces no
// borrowed slots at all and the bit arriving means the collision did.
enum RingRecordFlags : std::uint16_t {
kRecNone = 0,
kRecNeedsAck = 1u << 0,
+87
View File
@@ -0,0 +1,87 @@
// MobileGL - MobileGL/MG_Remote/Transport/RoleMemory.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Peak-RSS accounting for the two roles. Owner: package s1; CONSUMER: package t1,
// which puts the numbers in MEASUREMENTS.
//
// WHY BOTH HALVES ARE NEEDED, AND WHY NEITHER ALONE IS THE ANSWER.
//
// VmHWM is the kernel's own high-water mark of resident set size, in
// /proc/self/status. It is the only number that cannot be argued with - it
// counts what the process actually touched, including the pages the allocator
// never gave back. But under `inproc` BOTH ROLES ARE ONE PROCESS, so a single
// VmHWM cannot be split between them and reporting it as "the client's" would
// be a lie that only becomes visible in P6.
//
// The segment ledger is the other half: every ShmSegment this process mapped,
// by kind and by role. It is exact, it IS separable by role, and under `spawn`
// it is the part that appears in both processes at once (one mapping, two
// address spaces, one set of physical pages) - which is precisely the number a
// naive "sum the two VmHWMs" double-counts.
//
// So the pair is the measurement: VmHWM for what the process really cost, the
// ledger for how much of it is shared mapping that a second process will not pay
// for again. t1 reports both, per role, and the split's memory claim is
// (client VmHWM + server VmHWM - shared ledger), never either half on its own.
//
// A SAMPLE IS A SYSCALL AND A PARSE. Take it at phase boundaries - after the
// handshake, after the first frame, at teardown - never per record.
#pragma once
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
enum class MemoryRole : std::uint32_t {
Client = 0,
Server = 1,
kMemoryRoleCount = 2,
};
// Resident-set high-water mark of THIS PROCESS in bytes, from
// /proc/self/status's VmHWM line. 0 when the platform has no such file
// (Windows, and Android's /proc is readable but the caller should still
// treat 0 as "not measured" rather than "measured zero").
std::uint64_t ProcessPeakRssBytes();
// Current resident set (VmRSS), same source and same 0 convention. Sampled
// beside the peak so a phase that never grew the peak is distinguishable
// from one that was not sampled.
std::uint64_t ProcessCurrentRssBytes();
// The ledger. ShmSegment does NOT update it itself: a segment is also created
// by tests and by P6's adopt path, and a ledger that counted those would stop
// meaning "this session's footprint". The SESSION books its own segments.
void LedgerAddSegment(MemoryRole role, std::uint64_t bytes);
void LedgerRemoveSegment(MemoryRole role, std::uint64_t bytes);
std::uint64_t LedgerMappedBytes(MemoryRole role);
// Every role's mapped bytes. Under inproc the two roles map THE SAME pages,
// so this over-counts on purpose: the two per-role numbers are what t1
// subtracts with, and a single total that silently deduplicated them would
// hide exactly the spawn-vs-inproc difference the measurement is for.
std::uint64_t LedgerMappedBytesAllRoles();
// One sample, both halves, for one role.
struct RoleMemorySample {
std::uint64_t PeakRssBytes = 0;
std::uint64_t CurrentRssBytes = 0;
std::uint64_t MappedSegmentBytes = 0; // this role's ledger
MemoryRole Role = MemoryRole::Client;
};
RoleMemorySample SampleRoleMemory(MemoryRole role);
// Emits one line at INFO level, which is what every P5 lane builds at, so
// t1's harness can grep it out of a lane log without a new log sink. Not
// DEBUG, which the INFO build compiles out; not ERROR, which this is not.
// The grep tag is `MG_Remote memory[`.
// `phase` is a short tag: "handshake", "first-frame", "teardown".
void LogRoleMemory(const char* phase, const RoleMemorySample& sample);
} // namespace MobileGL::MG_Remote::Transport
+489
View File
@@ -0,0 +1,489 @@
// MobileGL - MobileGL/MG_Remote/Transport/SessionRings.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The ring-owning half of a session: the four segments, the two ring endpoints,
// and the five watermarks. Owner: package s1.
//
// This is the object the P5 gate is really about. ROADMAP.md:21 asks that
// `inproc` run THE SAME G3 codec as `spawn`, and InProcessTransport cannot
// deliver that no matter how it is edited: it is two deque<vector<uint8_t>> plus
// two condvar doorbells (InProcessTransport.cpp:38-97), it owns THE BELLS BUT
// NOT THE RING, and it runs no codec at all. So the rings live here, above
// ITransport, in one implementation both delivery modes use - and the transport
// supplies the control plane and the two bells, which is exactly what its own
// header says it is for (ITransport.h:16-20: "everything on the hot path
// bypasses this interface entirely").
//
// INPROC USES ShmSegment TOO, NOT new[]. In one address space a heap allocation
// would work and would be faster to write. It is refused deliberately: it is half
// of what makes "the same code path" true rather than nominal. A `new` here means
// the mapping, the alignment, the size rounding, the read-only peer view and the
// lifetime are all exercised for the first time in P6, on the day the second
// process appears - which is the shape of every "it was green in CI" failure this
// phase is trying not to repeat.
//
// ---------------------------------------------------------------------------
// THE KNOB NAMES THE RING; THE SEGMENT IS THE RING PLUS ONE CONTROL PAGE.
//
// Ring.h:11-13 puts RingControl at the HEAD of SEG_CMD and RingProducer requires
// a POWER-OF-TWO capacity (Ring.cpp:89-103 - the mask IS the indexing). Those two
// facts together mean a segment and its ring cannot both be 8 MiB, and one of the
// two numbers has to give.
//
// The one that gives is the SEGMENT: SEG_CMD is `MOBILEGL_IPC_RING_MB` MiB PLUS
// 4096, so the ring inside it is exactly MOBILEGL_IPC_RING_MB MiB and
// RingProducer::MaxRecordBytes() is exactly half of that. CONTRACT-P5 §5 and
// Config.h's MOBILEGL_IPC_RING_MB comment - "A RECORD MAY BE AT MOST HALF OF
// THIS, so 8 MiB caps one record at 4 MiB" - are then TRUE AS WRITTEN, which
// matters because that sentence is what every other package sizes against.
//
// The first version of this file did the opposite: an 8 MiB segment with a 4 MiB
// ring and a 2 MiB record cap, on the grounds that ProtocolSmokeTest.cpp:72 pinned
// the four announced sizes. That was wrong on the facts - that test builds four
// SegmentRefs from its own literals and round-trips them through the schema; it
// says nothing about what a session announces, and it never mentions
// SessionSegments at all. So the alternative was available at no cost, and the
// version that made two live documents false and left half of SEG_CMD mapped and
// unreachable was the worse of the two.
//
// SegmentRef.sizeBytes therefore announces the MAPPING size (ring + page), which
// is what a spawn peer must mmap. The four numbers a reader recognises - 8 MiB /
// 32 MiB / 8 MiB / 256 KiB - are the RING sizes, which is what the knobs name.
//
// SEG_STAGE has no control page of its own: RingControl carries TWO cursor
// triples (Ring.h:101-109) and the stage triple is the second. So SEG_STAGE is
// exactly its ring, and 32 MiB is already a power of two. SEG_REPLY is not a ring
// at all.
// ---------------------------------------------------------------------------
#pragma once
#include "Doorbell.h"
#include "EventRing.h"
#include "ReplySlot.h"
#include "Ring.h"
#include "RoleMemory.h"
#include "ShmSegment.h"
#include <atomic>
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// RING sizes, not segment sizes - see the header block. The four defaults are
// CONTRACT-P5's; MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB move the first
// two, and ServerSession applies them unless SetSegmentSizes overrode them.
struct SessionSegmentSizes {
std::uint64_t CmdRingBytes = 8ull * 1024 * 1024; // + one control page
// SEG_STAGE IS NOT A RING. Package w1's encoder owns it as an
// encoder-local LINEAR ALLOCATOR that reclaims on retiredSeq, so this is
// a plain byte count: no control page, and no rounding down to a power of
// two either. Rounding was a ring requirement and keeping it would have
// silently turned an operator's MOBILEGL_IPC_STAGE_MB=24 into 16.
std::uint64_t StageBytes = 32ull * 1024 * 1024;
std::uint64_t ReplyBytes = 8ull * 1024 * 1024; // slot pool, not a ring
std::uint64_t EventRingBytes = 256ull * 1024; // + one control page
std::uint32_t ReplySlotCount = kDefaultReplySlotCount;
};
// Largest power of two <= `bytes`, or 0 when there is none. The ring's
// indexing is a mask, so this is what a ring capacity has to be rounded to.
std::uint64_t LargestPowerOfTwoAtMost(std::uint64_t bytes);
// How big a segment has to be to hold `ringBytes` of ring behind its control
// page. `ringBytes` is rounded DOWN to a power of two first, so an operator
// who asks for 6 MiB gets a 4 MiB ring in a 4 MiB + 4096 segment rather than
// a segment whose tail can never be addressed.
std::uint64_t SegmentBytesForRing(std::uint64_t ringBytes);
// The usable ring inside a segment that carries a RingControl page at its
// head. The inverse of SegmentBytesForRing for any size it produced.
std::uint64_t RingCapacityForSegment(std::uint64_t segmentBytes);
enum class SessionSegmentSlot : std::uint32_t {
Cmd = 0,
Stage = 1,
Reply = 2,
Event = 3,
kSessionSegmentCount = 4,
};
// The four ShmSegments of one session, created and mapped read/write.
//
// WHO CREATES THEM: the SERVER, because Welcome announces all four
// (protocol.fbs's Welcome table) and Welcome is server -> client. "Client
// owned" in the schema's comments is about who WRITES a segment, not who
// allocates it. Under `inproc` the client then attaches to the same mapping
// (AttachInProcess); under `spawn` it will adopt the fds the server passed by
// SCM_RIGHTS, which is P6's and is why Adopt is on ShmSegment already.
class SessionSegments {
public:
SessionSegments() = default;
~SessionSegments();
SessionSegments(const SessionSegments&) = delete;
SessionSegments& operator=(const SessionSegments&) = delete;
// Creates and maps all four, initialises BOTH control pages (SEG_CMD's
// and SEG_EVENT's), and books the mapping in `role`'s ledger.
MobileGLResult Create(const SessionSegmentSizes& sizes, MemoryRole role);
// The inproc peer's view of the owner's four segments, booked under the
// OTHER role. It does NOT re-init the control pages - there is one shared
// page per ring and re-initialising it would zero the owner's cursors out
// from under whoever is already using them.
//
// ON POSIX THIS IS A REAL SECOND MAPPING, NOT AN ALIAS: each descriptor is
// dup()ed and adopted through ShmSegment::Adopt + Map, so the peer gets
// its own virtual addresses over the same memfd. That is the same reason
// inproc uses ShmSegment at all (see the header block): the ATTACH half is
// the half P6 replaces with an SCM_RIGHTS Adopt, and aliasing the owner's
// ShmSegment objects would leave it first exercised on the day the second
// process appears - which is exactly the criticism this file levels at
// new[]. It also removes a raw lifetime coupling: an aliased view holds
// pointers into the owner's members with no ownership, so the two Closes
// have to be ordered by hand.
//
// Windows has no Adopt (ShmSegment::Adopt is POSIX-only; the section name
// travels in SegmentRef instead), so there it still aliases and says so.
MobileGLResult AttachInProcess(SessionSegments& owner, MemoryRole role);
void Close();
bool Valid() const { return m_valid; }
RingControl* CmdControl() const { return m_cmdControl; }
void* CmdRingBase() const { return m_cmdRingBase; }
std::uint64_t CmdRingCapacity() const { return m_cmdRingCapacity; }
// The whole SEG_STAGE mapping, for w1's linear allocator and for the
// SegmentTable view the decoder resolves blobrefs against. There is no
// stage RING and no RingProducer/RingConsumer over RingCursorSet::Stage.
void* StageBase() const { return m_stageBase; }
std::uint64_t StageBytes() const { return m_stageBytes; }
void* ReplyBase() const { return m_replyBase; }
std::uint64_t ReplyBytes() const { return m_replyBytes; }
std::uint32_t ReplySlotCount() const { return m_replySlotCount; }
RingControl* EventControl() const { return m_eventControl; }
void* EventSegmentBase() const { return m_eventSegmentBase; }
void* EventRingBase() const { return m_eventRingBase; }
std::uint64_t EventRingCapacity() const { return m_eventRingCapacity; }
// For Welcome's four SegmentRefs. The announced size is the MAPPING size,
// which is what a peer must map - not the ring capacity inside it.
std::uint64_t AnnouncedSize(SessionSegmentSlot slot) const;
const char* AnnouncedName(SessionSegmentSlot slot) const;
int DescriptorFor(SessionSegmentSlot slot) const; // POSIX; -1 elsewhere
std::uint64_t MappedBytes() const { return m_mappedBytes; }
private:
void DeriveViews();
ShmSegment m_owned[4]; // empty on an attached (peer) view
ShmSegment* m_segments[4] = {nullptr, nullptr, nullptr, nullptr};
RingControl* m_cmdControl = nullptr;
void* m_cmdRingBase = nullptr;
std::uint64_t m_cmdRingCapacity = 0;
void* m_stageBase = nullptr;
std::uint64_t m_stageBytes = 0;
void* m_replyBase = nullptr;
std::uint64_t m_replyBytes = 0;
std::uint32_t m_replySlotCount = kDefaultReplySlotCount;
RingControl* m_eventControl = nullptr;
void* m_eventSegmentBase = nullptr;
void* m_eventRingBase = nullptr;
std::uint64_t m_eventRingCapacity = 0;
std::uint64_t m_mappedBytes = 0;
MemoryRole m_role = MemoryRole::Client;
bool m_valid = false;
bool m_owns = false;
bool m_booked = false;
};
// -----------------------------------------------------------------------
// The five watermarks (R-9). Every write and every wait goes through here,
// so the rules in Ring.h's header have exactly one implementation.
// -----------------------------------------------------------------------
//
// THE ONE RULE THAT MATTERS: a watermark may be published LATE but NEVER
// EARLY. Late costs a waiter some latency; early makes every waiter a silent
// use of work that has not happened, and there is no checksum anywhere on
// this ring that would catch it. The callers are responsible for never
// calling an advance before the work is done - that is the half only a
// call-site review and R-9's unit cases can enforce.
//
// THE HALF THAT IS MECHANICALLY DETECTABLE - a watermark moving BACKWARDS -
// IS FATAL, not logged-and-ignored. Logging it and returning was the first
// version of this file and it was worse than useless: SessionConsumer keeps
// its own counter, so once the shared appliedSeq is behind, every later
// advance is a no-op for ever and every WaitForApplied(seq, kWaitForever) -
// the verb barrier and every reply wait - blocks permanently. The user sees a
// hang and the only evidence is one ERROR line. This is the same class as
// RingCursorsValid returning false, and Ring.h:181-185 already calls that "a
// Fatal{ProtocolCorruption}, never a retry".
namespace Watermark {
// Producer, after Publish. Nobody waits on it - it is the answer to "how
// far ahead of the server is the client right now".
void AdvanceSubmitted(RingControl& control, std::uint64_t seq);
// Consumer, ONCE PER APPLIED RECORD. P5 forbids the 64-record batching
// this ring was designed for: the verb barrier and every reply wait read
// it. kRecPad does not count - RingConsumer::Pop skips fillers, so the
// rule is kept by counting Pops rather than bytes.
void AdvanceApplied(RingControl& control, std::uint64_t seq);
// Consumer, once the SEG_STAGE bytes a record referenced are finished
// with. The staging allocator reclaims behind it and nothing else may.
void AdvanceRetired(RingControl& control, std::uint64_t seq);
// Server, when a present completes. Trails appliedSeq by the GPU's own
// depth; never conflate the two.
void AdvanceCompletedFrame(RingControl& control, std::uint64_t serial);
// Server, when it returns a present credit. The only back-pressure that
// bounds latency rather than bytes.
void AdvancePresentAck(RingControl& control, std::uint64_t serial);
// Every wait is >=, never ==: both sides advance in jumps, and an
// equality waiter misses its wakeup and hangs until the next coincidence.
inline bool Reached(const std::atomic<std::uint64_t>& watermark, std::uint64_t target) {
return watermark.load(std::memory_order_acquire) >= target;
}
} // namespace Watermark
enum class SessionWait : std::uint32_t {
Reached = 0,
// The doorbell died: the peer shut the session down. The ONLY thing that
// can un-park a waiter on kWaitForever (Doorbell.h:211-221), and the
// reason a bounded join is possible at all.
ShutDown = 1,
TimedOut = 2,
};
// -----------------------------------------------------------------------
// The client's end of the rings.
//
// THE TWO DOORBELL ACCESSORS LIVE ON THE SESSION, NOT ON ITransport
// (contract §3.9, the ruling s1 is asked to make now rather than let P6
// discover). The session takes the two references InProcessTransport hands
// out and is the only thing that knows which is which; ITransport stays the
// dumb control-plane interface its header claims to be, and P6's
// SocketTransport does not grow two accessors it has no natural home for.
// -----------------------------------------------------------------------
class SessionProducer {
public:
SessionProducer() = default;
// `peerBell` is the bell the SERVER parks on and this side rings;
// `selfBell` is this side's own. InProcessTransport::PeerDoorbell() and
// SelfDoorbell() are exactly that pair, from the client endpoint.
// NO STAGE RING. SEG_STAGE is w1's encoder-local linear allocator and
// RingCursorSet::Stage has no producer and no consumer in P5 - see
// RingControl's stage triple in Ring.h. A producer here would publish
// stageHead with nothing advancing the two tails, so FreeBytes() would
// fall to zero the first time the head lapped the capacity and never
// recover: a guaranteed hang, not a slow path.
void Attach(RingControl* control, RingProducer* cmd, Doorbell* peerBell, Doorbell* selfBell,
std::uint32_t spinUs);
void Detach();
bool Valid() const { return m_control != nullptr && m_cmd != nullptr; }
// Publish the command ring's head, record submittedSeq, THEN ring - in
// that order and never any other. Doorbell.h:186-193: the fence only
// orders what precedes it, so ringing before publishing reopens the very
// lost-wakeup window the fences exist to close. RingTest.cpp:446 pins the
// call order; this is the one place production code performs it.
void PublishAndNotify(std::uint64_t submittedSeq);
// The last seq THIS producer published, kept locally rather than read back
// out of RingControl::submittedSeq. Teardown's drain needs it: Ring.h:72-77
// explicitly permits submittedSeq to be published LAZILY and Ring.h:243
// encourages batching the publish, so the shared watermark may lag the
// emitter - and a drain that waits for `appliedSeq >= submittedSeq` would
// then under-wait and free an emitter's var-tail while a record still
// names it. With the verb barrier armed the two are equal; with
// MOBILEGL_IPC_VERB_BARRIER=0, R-1's negative control which the phase has
// to run once, they are not.
std::uint64_t LastPublishedSeq() const { return m_lastPublishedSeq; }
// The verb barrier's wait, AND the reply's wait: they are the same wait
// (R-3/R-5), which is why a blocking ReadPixels, MapPersistent's decline
// and the four Bool acceptances cost ZERO extra round trips.
SessionWait WaitForApplied(std::uint64_t seq, std::uint32_t timeoutMs);
// Present throttle.
SessionWait WaitForPresentAck(std::uint64_t serial, std::uint32_t timeoutMs);
// Back-pressure when Reserve returned nullptr. NEVER call this when
// FreeBytes() is already >= the record: Ring.h:226-233 - a nullptr with
// enough free bytes can only mean "too big, chunk", and waiting on it
// stalls forever.
SessionWait WaitForCmdSpace(std::uint64_t bytes, std::uint32_t timeoutMs);
RingControl* Control() const { return m_control; }
RingProducer* Cmd() const { return m_cmd; }
Doorbell* PeerDoorbell() const { return m_peerBell; }
Doorbell* SelfDoorbell() const { return m_selfBell; }
std::uint32_t SpinUs() const { return m_spinUs; }
private:
template <class Ready>
SessionWait Park(Ready&& ready, std::uint32_t timeoutMs);
RingControl* m_control = nullptr;
RingProducer* m_cmd = nullptr;
Doorbell* m_peerBell = nullptr;
Doorbell* m_selfBell = nullptr;
std::uint32_t m_spinUs = kDefaultSpinUs;
std::uint64_t m_lastPublishedSeq = 0;
};
// -----------------------------------------------------------------------
// The server's end of the rings: the apply thread's loop, minus the applier.
// -----------------------------------------------------------------------
class SessionConsumer {
public:
SessionConsumer() = default;
void Attach(RingControl* control, RingConsumer* cmd, Doorbell* peerBell, Doorbell* selfBell,
std::uint32_t spinUs);
void Detach();
bool Valid() const { return m_control != nullptr && m_cmd != nullptr; }
// Park until a record is waiting, the session is shut down, or the
// deadline passes. Pass kWaitForever for the steady state; a dead bell is
// what ends it, which is why Doorbell::Kill() is load-bearing for the
// join (InProcessTransportTest.cpp:344 pins the shape).
SessionWait WaitForWork(std::uint32_t timeoutMs);
// Pop ONE record and hand it to `apply`. Returns false when the ring is
// empty. On a corrupt header it returns false and sets *outCorrupt, which
// the CALLER escalates to Fatal{ProtocolCorruption} rather than retrying.
//
// This is the only place appliedSeq is advanced, and it advances it by
// EXACTLY ONE per record - never a batch (R-9). kRecPad cannot reach
// `apply`: RingConsumer::Pop skips fillers before returning, so a filler
// is never counted here and the two sides' sequence spaces cannot drift.
// Order: apply -> appliedSeq -> PublishApplied -> ring the client.
template <class Apply>
bool ApplyOne(Apply&& apply, bool* outCorrupt = nullptr) {
if (outCorrupt != nullptr) {
*outCorrupt = false;
}
if (!Valid()) {
return false;
}
RingRecordView view{};
if (!m_cmd->Pop(view, outCorrupt)) {
return false;
}
apply(view);
++m_appliedSeq;
Watermark::AdvanceApplied(*m_control, m_appliedSeq);
m_cmd->PublishApplied();
// THE BYTE CURSOR RetireThrough MAY RECLAIM TO, which is NOT simply
// "everything popped". A record carrying kRecBorrowSlot has been lent
// into the GPU timeline and its slot can only be recycled after
// completedFrameSerial (Ring.h:24-27), so the reclaimable cursor stops
// AT the first borrowed record and does not move again until
// RetireBorrowedUpTo releases it. Nothing sets kRecBorrowSlot yet;
// this is here so that the day something does, the producer does not
// overwrite a slot the GPU is still reading.
if ((view.flags & kRecBorrowSlot) == 0 && !m_borrowHeld) {
m_retirableCursor = view.cursor + sizeof(RingRecordHeader) + view.payloadSize;
} else {
if ((view.flags & kRecBorrowSlot) != 0) {
// P5 PRODUCES NO BORROWED SLOTS AT ALL, so this bit arriving
// is either a borrow nobody implemented or MGPipeCallFlags::
// kHostSpan wearing kRecBorrowSlot's bit (Ring.h's collision
// table) - and P5's reduced path is ruled to produce zero
// host spans too. Either way the conservative arm is taken
// (nothing past it is reclaimed) and the sighting is NAMED,
// because the alternative is a producer that wedges on the
// first full ring with no line anywhere saying why.
NoteBorrowedRecord(view.kind, view.flags);
}
m_borrowHeld = true;
}
NotifyClient();
return true;
}
// Publish the retire watermark and hand back every byte up to the first
// still-borrowed record.
//
// IT IS MANDATORY, NOT OPTIONAL. RingProducer::FreeBytes() reclaims against
// retiredTail ONLY (Ring.cpp:110-115) and nothing else in this class
// publishes it, so an apply loop that calls ApplyOne and never this wedges
// the producer on the first full ring. Call it once per drain batch.
void RetireThrough(std::uint64_t seq);
// Release borrowed slots up to `cursor` once completedFrameSerial has
// passed them. `cursor` is a RingRecordView::cursor the apply loop kept.
// This is the only thing that moves the reclaim point past a borrowed
// record - see ApplyOne.
void RetireBorrowedUpTo(std::uint64_t cursor);
// The byte cursor RetireThrough would reclaim to right now. Diagnostic;
// a borrow that is never released shows up as this number standing still.
std::uint64_t RetirableCursor() const { return m_retirableCursor; }
// completedFrameSerial and presentAckSerial, advanced AND rung. The free
// functions in namespace Watermark advance only: a v1 caller that used one
// directly would leave a client parked in WaitForPresentAck(kWaitForever)
// with nothing to wake it, because the advance and the doorbell are two
// separate stores and only the pair is a wakeup.
void CompleteFrame(std::uint64_t serial);
void ReturnPresentCredit(std::uint64_t serial);
// Ring the client's bell, but only when it said it is parked: a store to
// a shared cache line otherwise burns a big core for a whole frame on a
// phone (Doorbell.h:13-22).
void NotifyClient();
std::uint64_t AppliedSeq() const { return m_appliedSeq; }
// How many kRecBorrowSlot records this consumer has seen. Non-zero in P5
// is a finding, not a statistic.
std::uint64_t BorrowedRecordsSeen() const { return m_borrowedSeen; }
RingControl* Control() const { return m_control; }
RingConsumer* Cmd() const { return m_cmd; }
Doorbell* PeerDoorbell() const { return m_peerBell; }
Doorbell* SelfDoorbell() const { return m_selfBell; }
std::uint32_t SpinUs() const { return m_spinUs; }
private:
RingControl* m_control = nullptr;
RingConsumer* m_cmd = nullptr;
Doorbell* m_peerBell = nullptr;
Doorbell* m_selfBell = nullptr;
std::uint32_t m_spinUs = kDefaultSpinUs;
std::uint64_t m_appliedSeq = 0;
std::uint64_t m_retirableCursor = 0;
std::uint64_t m_borrowedSeen = 0;
bool m_borrowHeld = false;
// Out of line so ApplyOne, which is a template in a header this layer
// keeps free of MobileGL/Includes.h, can still log.
void NoteBorrowedRecord(std::uint16_t kind, std::uint16_t flags);
};
// -----------------------------------------------------------------------
// The ABI fingerprint's mixer.
//
// It lives under Transport/ rather than in CapsCodec.cpp so that it can be
// tested without the GL frontend's umbrella header, and so that the SIZES it
// mixes are the caller's - CapsCodec.cpp passes the three real sizeofs, a
// unit test passes made-up ones and can then prove a one-byte difference
// changes the answer. A fingerprint that cannot be shown to change is
// indistinguishable from one that is never compared.
// -----------------------------------------------------------------------
std::uint64_t MixAbiFingerprint(std::uint64_t dynamicParamsSize, std::uint64_t capsSize,
std::uint64_t functionTableSize, std::uint32_t abiVersion,
const char* buildStamp);
} // namespace MobileGL::MG_Remote::Transport
+374
View File
@@ -8,12 +8,27 @@
// Platform-independent half of ShmSegment. The create/map/close bodies live in
// ShmSegmentPosix.cpp and ShmSegmentWin32.cpp.
//
// P5 adds two things that are about a SET of segments rather than about one:
// SessionSegments (the four a session owns, and the ring geometry derived from
// them) and the role memory ledger that t1 reports against.
#include "ShmSegment.h"
#include "RoleMemory.h"
#include "SessionRings.h"
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <utility>
#if !defined(_WIN32)
#include <unistd.h>
#endif
namespace MobileGL::MG_Remote::Transport {
ShmSegment::~ShmSegment() { Close(); }
@@ -46,4 +61,363 @@ namespace MobileGL::MG_Remote::Transport {
bool ShmSegment::Valid() const { return m_size != 0 && (m_fd >= 0 || m_nativeHandle != nullptr); }
// =======================================================================
// P5: the role memory ledger and the VmHWM sample
// =======================================================================
namespace {
constexpr std::size_t kMemoryRoleCount = static_cast<std::size_t>(MemoryRole::kMemoryRoleCount);
std::atomic<std::uint64_t>& LedgerSlot(MemoryRole role) {
static std::atomic<std::uint64_t> ledger[kMemoryRoleCount];
const std::size_t index = static_cast<std::size_t>(role);
return ledger[index < kMemoryRoleCount ? index : 0];
}
const char* RoleName(MemoryRole role) {
return role == MemoryRole::Server ? "server" : "client";
}
// One pass over /proc/self/status for a "VmHWM:" / "VmRSS:" line. The
// values are in kB and the unit suffix is part of the line, so it is
// parsed rather than assumed.
std::uint64_t ProcStatusBytes(const char* key) {
#if defined(__linux__) || defined(__ANDROID__)
std::FILE* file = std::fopen("/proc/self/status", "re");
if (file == nullptr) {
return 0;
}
const std::size_t keyLength = std::strlen(key);
char line[256];
std::uint64_t bytes = 0;
while (std::fgets(line, sizeof(line), file) != nullptr) {
if (std::strncmp(line, key, keyLength) != 0) {
continue;
}
unsigned long long kilobytes = 0;
// The format is "<key>:\t <number> kB". Anything else is a
// kernel this code has not seen, and 0 ("not measured") is the
// honest answer for it.
if (std::sscanf(line + keyLength, ": %llu kB", &kilobytes) == 1) {
bytes = static_cast<std::uint64_t>(kilobytes) * 1024ull;
}
break;
}
std::fclose(file);
return bytes;
#else
(void)key;
return 0;
#endif
}
} // namespace
std::uint64_t ProcessPeakRssBytes() { return ProcStatusBytes("VmHWM"); }
std::uint64_t ProcessCurrentRssBytes() { return ProcStatusBytes("VmRSS"); }
void LedgerAddSegment(MemoryRole role, std::uint64_t bytes) {
LedgerSlot(role).fetch_add(bytes, std::memory_order_relaxed);
}
void LedgerRemoveSegment(MemoryRole role, std::uint64_t bytes) {
std::atomic<std::uint64_t>& slot = LedgerSlot(role);
const std::uint64_t current = slot.load(std::memory_order_relaxed);
// Clamped rather than wrapped: a double-unbook would otherwise report a
// role holding sixteen exabytes, which is a number nobody reads as a bug.
slot.store(bytes > current ? 0 : current - bytes, std::memory_order_relaxed);
}
std::uint64_t LedgerMappedBytes(MemoryRole role) {
return LedgerSlot(role).load(std::memory_order_relaxed);
}
std::uint64_t LedgerMappedBytesAllRoles() {
std::uint64_t total = 0;
for (std::size_t index = 0; index < kMemoryRoleCount; ++index) {
total += LedgerSlot(static_cast<MemoryRole>(index)).load(std::memory_order_relaxed);
}
return total;
}
RoleMemorySample SampleRoleMemory(MemoryRole role) {
RoleMemorySample sample;
sample.Role = role;
sample.PeakRssBytes = ProcessPeakRssBytes();
sample.CurrentRssBytes = ProcessCurrentRssBytes();
sample.MappedSegmentBytes = LedgerMappedBytes(role);
return sample;
}
void LogRoleMemory(const char* phase, const RoleMemorySample& sample) {
// INFO, not DEBUG: t1 greps this out of a lane log and MGLOG_D is compiled out at the
// INFO level every P5 lane builds at. It is a handful of lines per session - the
// handshake, the first frame and teardown - so it is not per-frame noise either.
//
// VmHWM is the PROCESS's, so under inproc both roles report the same
// number and only the ledger differs. The line says so rather than
// leaving a reader to work out why two roles have one peak.
MGLOG_I("MG_Remote memory[%s/%s]: peakRss=%llu currentRss=%llu roleMapped=%llu "
"allRolesMapped=%llu (peakRss is the PROCESS's; under inproc both roles share it)",
phase == nullptr ? "?" : phase, RoleName(sample.Role),
static_cast<unsigned long long>(sample.PeakRssBytes),
static_cast<unsigned long long>(sample.CurrentRssBytes),
static_cast<unsigned long long>(sample.MappedSegmentBytes),
static_cast<unsigned long long>(LedgerMappedBytesAllRoles()));
}
// =======================================================================
// P5: SessionSegments
// =======================================================================
namespace {
constexpr std::size_t kSlotCount =
static_cast<std::size_t>(SessionSegmentSlot::kSessionSegmentCount);
std::size_t SlotIndex(SessionSegmentSlot slot) {
const std::size_t index = static_cast<std::size_t>(slot);
return index < kSlotCount ? index : 0;
}
} // namespace
SessionSegments::~SessionSegments() { Close(); }
MobileGLResult SessionSegments::Create(const SessionSegmentSizes& sizes, MemoryRole role) {
Close();
// Set BEFORE the loop, so that Close() on a failure INSIDE it really
// closes what has already been created: Close only walks m_owned when
// m_owns is true, and setting it afterwards left a failure at segment 3
// holding segments 0-2's descriptors and mappings open with Valid()
// false, against ShmSegment.h:66's "unmaps and releases the descriptor".
m_owns = true;
struct Spec {
const char* name;
std::uint64_t bytes;
};
// The sizes are RING sizes; a segment that carries a control page at its
// head is that much bigger. SEG_STAGE drives the SECOND cursor triple of
// SEG_CMD's page and SEG_REPLY is not a ring at all, so neither of those
// two grows.
const Spec specs[kSlotCount] = {
{"mgl-cmd", SegmentBytesForRing(sizes.CmdRingBytes)},
{"mgl-stage", sizes.StageBytes},
{"mgl-reply", sizes.ReplyBytes},
{"mgl-event", SegmentBytesForRing(sizes.EventRingBytes)},
};
for (std::size_t index = 0; index < kSlotCount; ++index) {
if (specs[index].bytes == 0) {
MGLOG_E("MG_Remote session: segment %s was asked for a ring size that cannot be "
"made into one (a ring is a power of two between %llu and %llu bytes)",
specs[index].name, static_cast<unsigned long long>(kMinRingCapacity),
static_cast<unsigned long long>(kMaxRingCapacity));
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
}
for (std::size_t index = 0; index < kSlotCount; ++index) {
const MobileGLResult created =
ShmSegment::Create(specs[index].name, specs[index].bytes, m_owned[index]);
if (created != MOBILEGL_OK) {
MGLOG_E("MG_Remote session: could not create segment %s of %llu bytes (rc=%d)",
specs[index].name, static_cast<unsigned long long>(specs[index].bytes),
static_cast<int>(created));
Close();
return created;
}
// Read/write on both roles under inproc: they are the same mapping.
// P6's read-only peer view is a property of the ADOPT path, not of
// this one, and pretending otherwise here would give the inproc lane
// a protection the spawn lane does not reproduce.
const MobileGLResult mapped = m_owned[index].Map(false);
if (mapped != MOBILEGL_OK) {
MGLOG_E("MG_Remote session: could not map segment %s (rc=%d)", specs[index].name,
static_cast<int>(mapped));
Close();
return mapped;
}
}
m_replySlotCount = sizes.ReplySlotCount;
DeriveViews();
if (!m_valid) {
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Both control pages, zeroed with their generations at 1. The owner does
// this exactly once; the inproc peer must NOT, or it would zero the
// cursors out from under whoever is already using them.
InitRingControl(*m_cmdControl);
InitRingControl(*m_eventControl);
m_role = role;
LedgerAddSegment(role, m_mappedBytes);
m_booked = true;
return MOBILEGL_OK;
}
MobileGLResult SessionSegments::AttachInProcess(SessionSegments& owner, MemoryRole role) {
Close();
if (!owner.Valid()) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
#if !defined(_WIN32)
// A REAL SECOND MAPPING, not an alias. dup + Adopt + Map is byte for byte
// the call sequence P6's SCM_RIGHTS client runs, so mapping, the fstat
// size check inside Adopt (ShmSegmentPosix.cpp:119-141), alignment and the
// peer's own lifetime are all exercised now rather than on the day the
// second process appears. Aliasing the owner's ShmSegment objects would
// leave the attach half untested for exactly the reason this file refuses
// to allocate the rings with new[].
m_owns = true;
for (std::size_t index = 0; index < kSlotCount; ++index) {
const ShmSegment* theirs = owner.m_segments[index];
const int duplicate = theirs == nullptr ? -1 : ::dup(theirs->Fd());
if (duplicate < 0) {
MGLOG_E("MG_Remote session: could not dup the owner's descriptor for segment %zu",
index);
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Adopt takes ownership of `duplicate` on success only.
const MobileGLResult adopted =
ShmSegment::Adopt(duplicate, theirs->Size(), m_owned[index]);
if (adopted != MOBILEGL_OK) {
::close(duplicate);
Close();
return adopted;
}
// Read/write: under inproc the client writes SEG_CMD and SEG_STAGE and
// reads SEG_REPLY and SEG_EVENT, and one ShmSegment maps the whole
// thing one way. The per-segment read-only peer view is P6's, where
// the roles are separable.
const MobileGLResult mapped = m_owned[index].Map(false);
if (mapped != MOBILEGL_OK) {
Close();
return mapped;
}
}
#else
// Windows has no Adopt (ShmSegment::Adopt is POSIX-only; a Windows peer
// resolves the section by the name carried in SegmentRef). Alias, and say
// so: this arm does not exercise the attach path P6 replaces.
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_segments[index] = owner.m_segments[index];
}
m_owns = false;
#endif
m_replySlotCount = owner.m_replySlotCount;
DeriveViews();
if (!m_valid) {
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_role = role;
// Booked under this role as well, and that double-counting is the point:
// the pages are shared under inproc and are NOT shared under spawn, so
// the per-role numbers are what t1 subtracts with.
LedgerAddSegment(role, m_mappedBytes);
m_booked = true;
return MOBILEGL_OK;
}
void SessionSegments::DeriveViews() {
m_valid = false;
if (m_owns) {
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_segments[index] = &m_owned[index];
}
}
for (std::size_t index = 0; index < kSlotCount; ++index) {
if (m_segments[index] == nullptr || m_segments[index]->Data() == nullptr) {
return;
}
}
auto* cmdBase = static_cast<std::uint8_t*>(m_segments[0]->Data());
m_cmdControl = reinterpret_cast<RingControl*>(cmdBase);
m_cmdRingBase = cmdBase + sizeof(RingControl);
m_cmdRingCapacity = RingCapacityForSegment(m_segments[0]->Size());
// SEG_STAGE IS NOT A RING: no control page, no cursor triple, no power-of-
// two rounding. Package w1's encoder owns it as a linear allocator that
// reclaims on retiredSeq, so the whole mapping is usable bytes.
m_stageBase = m_segments[1]->Data();
m_stageBytes = m_segments[1]->Size();
m_replyBase = m_segments[2]->Data();
m_replyBytes = m_segments[2]->Size();
auto* eventBase = static_cast<std::uint8_t*>(m_segments[3]->Data());
m_eventSegmentBase = eventBase;
m_eventControl = reinterpret_cast<RingControl*>(eventBase);
m_eventRingBase = eventBase + sizeof(RingControl);
m_eventRingCapacity = RingCapacityForSegment(m_segments[3]->Size());
m_mappedBytes = 0;
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_mappedBytes += m_segments[index]->Size();
}
if (m_cmdRingCapacity == 0 || m_stageBytes == 0 || m_eventRingCapacity == 0 ||
m_replyBytes == 0) {
MGLOG_E("MG_Remote session: segment sizes leave no usable ring (cmd cap=%llu stage "
"cap=%llu event cap=%llu reply=%llu). A ring is the largest POWER OF TWO that "
"fits after the 4096 byte control page, so a segment must be strictly larger "
"than one page plus the smallest ring",
static_cast<unsigned long long>(m_cmdRingCapacity),
static_cast<unsigned long long>(m_stageBytes),
static_cast<unsigned long long>(m_eventRingCapacity),
static_cast<unsigned long long>(m_replyBytes));
return;
}
m_valid = true;
}
void SessionSegments::Close() {
if (m_booked) {
LedgerRemoveSegment(m_role, m_mappedBytes);
m_booked = false;
}
if (m_owns) {
for (ShmSegment& segment : m_owned) {
segment.Close();
}
}
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_segments[index] = nullptr;
}
m_cmdControl = nullptr;
m_cmdRingBase = nullptr;
m_cmdRingCapacity = 0;
m_stageBase = nullptr;
m_stageBytes = 0;
m_replyBase = nullptr;
m_replyBytes = 0;
m_eventControl = nullptr;
m_eventSegmentBase = nullptr;
m_eventRingBase = nullptr;
m_eventRingCapacity = 0;
m_mappedBytes = 0;
m_owns = false;
m_valid = false;
}
std::uint64_t SessionSegments::AnnouncedSize(SessionSegmentSlot slot) const {
const ShmSegment* segment = m_segments[SlotIndex(slot)];
return segment == nullptr ? 0 : segment->Size();
}
const char* SessionSegments::AnnouncedName(SessionSegmentSlot slot) const {
const ShmSegment* segment = m_segments[SlotIndex(slot)];
return segment == nullptr ? "" : segment->Name();
}
int SessionSegments::DescriptorFor(SessionSegmentSlot slot) const {
const ShmSegment* segment = m_segments[SlotIndex(slot)];
return segment == nullptr ? -1 : segment->Fd();
}
} // namespace MobileGL::MG_Remote::Transport
+3
View File
@@ -9,6 +9,9 @@ set(MOBILEGL_WIRE_TESTS
RingTest
InProcessTransportTest
ProtocolSmokeTest
# P5 s1: the ring-owning session pair - four ShmSegment-backed segments, the five
# watermarks with real writers, the reply slot pool and the event ring.
SessionTest
)
if (NOT WIN32)
File diff suppressed because it is too large Load Diff