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