[Feat] (MG_Remote, Transport): the ring-owning session pair over ShmSegment - four segments, the five watermarks with their first real writers, the SEG_REPLY slot pool and the SEG_EVENT reverse channel, with the doorbell pair taken from the transport and the ring capacity derived as the largest power of two left after the control page

This commit is contained in:
2026-09-11 14:13:07 -04:00
parent ff2994d9a8
commit 817e1c40cf
6 changed files with 1474 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
// MobileGL - MobileGL/MG_Remote/Transport/EventRing.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SEG_EVENT: the server -> client reverse channel. Owner: package s1.
//
// WHAT P5 OWES HERE AND NOTHING MORE (BRIEF R-12, and s1's brief): it must be
// able to CARRY OnBufferWriteback, OnGpuWritten and OnSurfaceChanged. The other
// seven MGPipeCallbacks members are off P5's reduced path, and THE OVERFLOW
// POLICY IS P9's - what is here is the mechanism (a full ring latches
// eventRingFull, a lossy post that is dropped counts in eventDropped) and not a
// policy that decides between them.
//
// IT IS A SECOND RingControl, NOT A THIRD CURSOR SET. RingControl carries two
// cursor triples (SEG_CMD and SEG_STAGE) and adding a third would resize the
// shared page that Ring.h static_asserts at exactly 4096 bytes. So SEG_EVENT
// gets its OWN control page at its own head and drives it with the Cmd cursor
// set - the same RingProducer/RingConsumer code, in the opposite direction. The
// two EVENT FLAGS still live in the SEG_CMD page, because that is where Ring.h
// declares them and where the client's own waits already look.
//
// THE PAYLOAD SHAPES ARE FIXED-WIDTH AND LIVE HERE, not in MG_Pipe's headers.
// Nothing under Transport/ may reach MobileGL/Includes.h (WireLog.h:9-24, and
// the purity gate's `wire-header` probe), so MGPipeHandle / MGPRange /
// MGPSurfaceInfo cannot be named in this file. The wire shapes below mirror them
// field for field and the SESSIONS assert the two agree, which is the same
// discipline the rest of the wire is held to: a wire struct is fixed-width, and
// the conversion happens where the frontend types are legal.
#pragma once
#include "Doorbell.h"
#include "Ring.h"
#include <cstdint>
#include <cstring>
namespace MobileGL::MG_Remote::Transport {
// Record kinds on SEG_EVENT. 0 is kRingPadRecordKind and can never be an
// event, which is why the list starts at 1.
enum EventKind : std::uint16_t {
kEventNone = 0,
kEventBufferWriteback = 1, // MGPipeCallbacks::OnBufferWriteback
kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten
kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged
};
// The 8-byte {slot, gen} pair, mirrored (MGPipeHandles.h:54-65).
struct EventHandle {
std::uint32_t Slot;
std::uint32_t Gen;
};
static_assert(sizeof(EventHandle) == 8, "the handle is the 8-byte {slot, gen} pair");
// MGPRange, mirrored (MGPipeTypes.h:63-67).
struct EventRange {
std::uint64_t Offset;
std::uint64_t Size;
};
static_assert(sizeof(EventRange) == 16, "MGPRange is 16 bytes on the wire");
// OnBufferWriteback(res, offset, MGPBlobRef bytes). The bytes follow this
// head INSIDE THE RECORD: the blobref the client hands the frontend names
// SEG_EVENT and the in-segment offset of those inline bytes, which is what
// makes "the destination is the client's shadow" (contract table 1 row 22)
// reachable without a second segment. `Size` is therefore the record's own
// tail length and is cross-checked against it.
struct EventBufferWritebackHead {
EventHandle Resource;
std::uint64_t Offset; // destination offset inside the resource
std::uint64_t Size; // inline byte count that follows
};
static_assert(sizeof(EventBufferWritebackHead) == 24, "wire shape");
// OnGpuWritten(res, rangeCount, ranges). EventRange[RangeCount] follows.
struct EventGpuWrittenHead {
EventHandle Resource;
std::uint32_t RangeCount;
std::uint32_t Pad0;
};
static_assert(sizeof(EventGpuWrittenHead) == 16, "wire shape");
// OnSurfaceChanged(const MGPSurfaceInfo*), mirrored (MGPipeTypes.h:1394-1401).
struct EventSurfaceChangedHead {
std::uint32_t Width;
std::uint32_t Height;
std::uint32_t InternalFormat;
std::uint16_t Samples;
std::uint16_t Layers;
std::uint8_t IsDefault;
std::uint8_t Pad0[7];
};
static_assert(sizeof(EventSurfaceChangedHead) == 24, "MGPSurfaceInfo is 24 bytes on the wire");
// The server's end. One producer: the apply thread, by construction.
class EventRingProducer {
public:
EventRingProducer() = default;
// `eventControl` is SEG_EVENT's own control page; `cmdControl` is the
// SEG_CMD page, because Ring.h declares eventRingFull / eventDropped
// there and the client's waits already look at it.
EventRingProducer(RingControl* eventControl, RingControl* cmdControl, void* base,
std::uint64_t capacityBytes)
: m_cmdControl(cmdControl),
m_producer(eventControl, base, capacityBytes, RingCursorSet::Cmd) {}
bool Valid() const { return m_producer.Valid() && m_cmdControl != nullptr; }
// Reserves one event record. nullptr means the ring is full: the caller
// decides, and the two flags are how it says which decision it took.
// THIS FUNCTION DOES NOT DECIDE - that is P9's.
void* Reserve(EventKind kind, std::uint64_t payloadBytes) {
void* slot = m_producer.Reserve(static_cast<std::uint16_t>(kind), kRecNone, payloadBytes);
if (slot == nullptr && m_cmdControl != nullptr) {
// "SEG_EVENT full, server stopped applying" - Ring.h:123. Latched
// here, cleared by the consumer once it has drained.
m_cmdControl->eventRingFull.store(1, std::memory_order_release);
}
return slot;
}
// For a LOSSY event the caller could not place. Lossless events must
// never call this; they wait for the client to drain instead.
void CountDrop() {
if (m_cmdControl != nullptr) {
m_cmdControl->eventDropped.fetch_add(1, std::memory_order_relaxed);
}
}
// Publish, THEN ring - the same order as the forward direction, and for
// the same reason (Doorbell.h:186-193: the fence only orders what
// precedes it, so ringing first reopens the lost-wakeup window).
void PublishAndNotify(Doorbell& clientBell, std::atomic<std::uint32_t>& producerParked) {
m_producer.Publish();
NotifyIfParked(clientBell, producerParked);
}
RingProducer& Ring() { return m_producer; }
private:
RingControl* m_cmdControl = nullptr;
RingProducer m_producer;
};
// The client's end. One consumer: the GL thread, which drains between verbs.
class EventRingConsumer {
public:
EventRingConsumer() = default;
EventRingConsumer(RingControl* eventControl, RingControl* cmdControl, void* base,
std::uint64_t capacityBytes, const void* segmentBase)
: m_cmdControl(cmdControl),
m_consumer(eventControl, base, capacityBytes, RingCursorSet::Cmd),
m_segmentBase(static_cast<const std::uint8_t*>(segmentBase)) {}
bool Valid() const { return m_consumer.Valid() && m_cmdControl != nullptr; }
bool Pop(RingRecordView& out, bool* outCorrupt = nullptr) {
return m_consumer.Pop(out, outCorrupt);
}
// Release the bytes and clear the full latch. Only after the caller has
// finished with every payload pointer it popped: a writeback's bytes live
// in the ring itself, so retiring early is the R-11 violation one level
// down.
void Drained() {
m_consumer.PublishRetired();
if (m_cmdControl != nullptr) {
m_cmdControl->eventRingFull.store(0, std::memory_order_release);
}
}
// Byte offset of `payload` inside SEG_EVENT, which is what an
// OnBufferWriteback MGPBlobRef must carry (Seg = kSegEvent, Offset =
// this, Size = the head's Size). Never a host address - R-2's rule B.
std::uint64_t OffsetInSegment(const void* payload) const {
return static_cast<std::uint64_t>(static_cast<const std::uint8_t*>(payload) -
m_segmentBase);
}
std::uint64_t DroppedEvents() const {
return m_cmdControl == nullptr
? 0
: m_cmdControl->eventDropped.load(std::memory_order_relaxed);
}
bool RingIsFull() const {
return m_cmdControl != nullptr &&
m_cmdControl->eventRingFull.load(std::memory_order_acquire) != 0;
}
RingConsumer& Ring() { return m_consumer; }
private:
RingControl* m_cmdControl = nullptr;
RingConsumer m_consumer;
const std::uint8_t* m_segmentBase = nullptr;
};
} // namespace MobileGL::MG_Remote::Transport
+251
View File
@@ -0,0 +1,251 @@
// 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>
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;
}
m_base = static_cast<std::uint8_t*>(base);
m_size = sizeBytes;
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::uint64_t m_size = 0;
std::uint32_t m_slots = 0;
std::uint32_t m_mask = 0;
std::uint32_t m_slotBytes = 0;
};
} // namespace MobileGL::MG_Remote::Transport
+255
View File
@@ -8,6 +8,8 @@
#include "Ring.h"
#include "SessionRings.h"
#include <MG_Util/Debug/Log.h>
#include <cstring>
@@ -303,4 +305,257 @@ 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 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, so it is refused loudly here; "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_E("MG_Remote watermark: refusing to move %s backwards, %llu -> %llu; a "
"waiter that already resumed on the higher value cannot be un-resumed",
name, static_cast<unsigned long long>(current),
static_cast<unsigned long long>(to));
return;
}
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, RingProducer* stage,
Doorbell* peerBell, Doorbell* selfBell, std::uint32_t spinUs) {
m_control = control;
m_cmd = cmd;
m_stage = stage;
m_peerBell = peerBell;
m_selfBell = selfBell;
m_spinUs = spinUs;
}
void SessionProducer::Detach() {
m_control = nullptr;
m_cmd = nullptr;
m_stage = nullptr;
m_peerBell = nullptr;
m_selfBell = nullptr;
}
void SessionProducer::PublishAndNotify(std::uint64_t submittedSeq) {
if (!Valid()) {
return;
}
// 1. the records themselves.
m_cmd->Publish();
if (m_stage != nullptr) {
m_stage->Publish();
}
// 2. the diagnostic watermark, after the bytes it describes.
Watermark::AdvanceSubmitted(*m_control, submittedSeq);
// 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);
}
SessionWait SessionProducer::WaitForStageSpace(std::uint64_t bytes, std::uint32_t timeoutMs) {
if (!Valid() || m_stage == nullptr) {
return SessionWait::TimedOut;
}
RingProducer* stage = m_stage;
return Park([stage, bytes] { return stage->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);
}
void SessionConsumer::Detach() {
m_control = nullptr;
m_cmd = nullptr;
m_peerBell = nullptr;
m_selfBell = nullptr;
}
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);
m_cmd->PublishRetired();
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
+85
View File
@@ -0,0 +1,85 @@
// 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 ERROR level (the wire layer's only level - WireLog.h) so
// t1's harness can grep it out of a lane log without a new log sink.
// `phase` is a short tag: "handshake", "first-frame", "teardown".
void LogRoleMemory(const char* phase, const RoleMemorySample& sample);
} // namespace MobileGL::MG_Remote::Transport
+369
View File
@@ -0,0 +1,369 @@
// 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
+309
View File
@@ -8,9 +8,20 @@
// 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>
@@ -46,4 +57,302 @@ 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();
struct Spec {
const char* name;
std::uint64_t bytes;
};
const Spec specs[kSlotCount] = {
{"mgl-cmd", sizes.CmdBytes},
{"mgl-stage", sizes.StageBytes},
{"mgl-reply", sizes.ReplyBytes},
{"mgl-event", sizes.EventBytes},
};
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_owns = true;
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;
}
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_segments[index] = owner.m_segments[index];
}
m_owns = false;
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 carries no control page of its own: RingControl holds TWO
// cursor triples and the stage triple is the second (Ring.h:106-109).
m_stageBase = m_segments[1]->Data();
m_stageCapacity = LargestPowerOfTwoAtMost(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_stageCapacity == 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_stageCapacity),
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_stageCapacity = 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