mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Merge] (MGPipe, P5): integrate package s1
# Conflicts: # MobileGL/MG_Remote/CapsCodec.cpp
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user