mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 16:48:31 +09:00
370 lines
18 KiB
C++
370 lines
18 KiB
C++
// 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 RING CAPACITY IS HALF THE SEGMENT, AND THAT IS ARITHMETIC, NOT A CHOICE.
|
|
//
|
|
// 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). A segment
|
|
// of 8 MiB therefore has 8 MiB - 4096 bytes left for records, and the largest
|
|
// power of two that fits is 4 MiB. A record may be at most half the ring
|
|
// (RingProducer::MaxRecordBytes), so the real cap on one record is 2 MiB.
|
|
//
|
|
// CONTRACT-P5 §5 and Config.h's MOBILEGL_IPC_RING_MB comment both say "8 MiB caps
|
|
// one record at 4 MiB". That arithmetic assumed the whole segment is ring bytes
|
|
// and did not subtract the control page. The number here is HALF of theirs, and
|
|
// the deviation is deliberately in the SAFE direction: R-10's obligation is to
|
|
// PROVE no record ever approaches the cap, and a lower cap makes that proof fire
|
|
// earlier and louder rather than later and silently. The alternatives were both
|
|
// worse - announcing SegmentRef.sizeBytes as 4096 + 8 MiB breaks the four sizes
|
|
// ProtocolSmokeTest.cpp:72 pins, and moving RingControl out of SEG_CMD needs a
|
|
// fifth SegmentRef that Welcome does not have.
|
|
//
|
|
// 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's
|
|
// capacity is its whole segment, and 32 MiB is already a power of two.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#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 {
|
|
|
|
// The four sizes are CONTRACT-P5's and are pinned by ProtocolSmokeTest.cpp:72.
|
|
// MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB move the first two.
|
|
struct SessionSegmentSizes {
|
|
std::uint64_t CmdBytes = 8ull * 1024 * 1024;
|
|
std::uint64_t StageBytes = 32ull * 1024 * 1024;
|
|
std::uint64_t ReplyBytes = 8ull * 1024 * 1024;
|
|
std::uint64_t EventBytes = 256ull * 1024;
|
|
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 any segment's usable ring area is.
|
|
std::uint64_t LargestPowerOfTwoAtMost(std::uint64_t bytes);
|
|
|
|
// Usable ring capacity of a segment that carries a RingControl page at its
|
|
// head. See the header block above for why this is half the segment.
|
|
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: the SAME mapping, booked under the OTHER role.
|
|
// It does not re-init the control pages - there is one shared page and
|
|
// re-initialising it would zero the owner's cursors under it.
|
|
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; }
|
|
|
|
void* StageBase() const { return m_stageBase; }
|
|
std::uint64_t StageCapacity() const { return m_stageCapacity; }
|
|
|
|
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_stageCapacity = 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. So the advances below REFUSE to move a
|
|
// watermark backwards (that is the detectable half) and the callers are
|
|
// responsible for never calling them before the work is done (that is the
|
|
// half only a call-site review and R-9's unit cases can enforce).
|
|
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.
|
|
void Attach(RingControl* control, RingProducer* cmd, RingProducer* stage, 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 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);
|
|
SessionWait WaitForStageSpace(std::uint64_t bytes, std::uint32_t timeoutMs);
|
|
|
|
RingControl* Control() const { return m_control; }
|
|
RingProducer* Cmd() const { return m_cmd; }
|
|
RingProducer* Stage() const { return m_stage; }
|
|
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;
|
|
RingProducer* m_stage = nullptr;
|
|
Doorbell* m_peerBell = nullptr;
|
|
Doorbell* m_selfBell = nullptr;
|
|
std::uint32_t m_spinUs = kDefaultSpinUs;
|
|
};
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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();
|
|
NotifyClient();
|
|
return true;
|
|
}
|
|
|
|
// Records without kRecBorrowSlot retire as soon as they are applied; a
|
|
// borrowed slot retires on completedFrameSerial, which is why this is a
|
|
// separate call and not folded into ApplyOne.
|
|
void RetireThrough(std::uint64_t seq);
|
|
|
|
// 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; }
|
|
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;
|
|
};
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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
|