Files
MobileGL/MobileGL/MG_Remote/Client/ClientSession.h
T

635 lines
40 KiB
C++

// MobileGL - MobileGL/MG_Remote/Client/ClientSession.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 client half of a session: the rings, the handshake and the record wait rule
// (ARCHITECTURE.md §11.6). Inproc uses the same shared segments, ring and codec as spawn;
// InProcessTransport supplies only the two doorbells and the control plane, so the inproc lane
// exercises the real data path. The reply slot id is the record seq, so waiting for
// appliedSeq >= seq is also the wait for the answer and a reply costs no extra round trip.
// An answer the server may decline is read from its reply, never assumed: a client that assumed
// "accepted" would clear its dirty flags for records the server dropped. Rows that skip the
// answer (willReadReply=false) do so only where the client already knows it or nobody uses it.
#pragma once
#include "../Transport/ILink.h"
#include "../Transport/ControlInbox.h"
#include <Includes.h>
#include <atomic> // the device-lost latch is read off the GL thread
#include <Config.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Util/Damage/Damage.h>
#include "../Server/ServerSpawn.h"
#include "../Server/SurfaceControlFrame.h"
#include "../Transport/Doorbell.h"
#include "../Transport/EventRing.h"
#include "../Transport/ITransport.h"
#include "../Transport/ReplySlot.h"
#include "../Transport/Ring.h"
#include "../Transport/RoleMemory.h"
#include "../Transport/SessionRings.h"
#include "../Transport/SocketTransport.h"
#include "../Transport/AdoptT0.h"
#include "../Wire/PipeWireCodec.h"
#include "CapsMirror.h"
#include <functional>
#include <unordered_map>
#include <memory>
namespace MobileGL::MG_Remote::Transport {
class InProcessTransport;
}
namespace MobileGL::MG_Remote::Client {
class ClientSession {
public:
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
static ClientSession* Active();
// Whether the device is lost for the calling thread. The latch is per session: set at
// most once, never cleared while that session lives. Recovery replaces the session
// (RecoverAfterDeviceLoss) and every context built before stays lost (ARCHITECTURE.md
// §15.3). It is set only when the peer hangs up or the server sends a SessionFault, never
// from a timeout: under run-ahead a server a frame behind is healthy. Also true inside
// ScopedWireMute and on a thread whose context belongs to a replaced session;
// ScopedCurrentSessionWork lifts the thread's staleness, not the latch. Otherwise false
// when no session is active (monolith).
static Bool DeviceLost();
// The latch alone: is the ACTIVE session lost. What the EGL layer asks before recovering.
static Bool SessionLatchedLost();
// Arms the latch and says why, once. Safe to call repeatedly and from any thread; only
// the first call logs.
void LatchDeviceLost(const char* why);
// Why the most recent session of this process was lost, in the latch's words (empty
// before any loss). Kept past the recovery, because the contexts of the lost session
// outlive it: glGetGraphicsResetStatus names it when it first reports a context's reset.
static String LastLossReason();
// A reply-owning record came back DECLINED where only a latched server declines (a
// readback). The server publishes its SessionFault on the control plane BEFORE it declines,
// so the frame - or the hangup that follows it - is what tells "lost" from a refusal. Pumps
// the control plane and asks the bell for up to `waitMs`; true when the session is lost.
Bool ConfirmLossAfterDecline(Uint32 waitMs);
// THE RECOVERY. Ends this lost session (Stop) and dials the configured server for a fresh
// one (StartSpawned); the new session forgets every client-side cache of the old one and
// advances MG_State's wire epoch, which is what makes every older GLContext lost. The
// caller (EGLImpl) holds the EGL operation lock, which every GL entry point also takes, so
// nothing else is emitting. Refused by name - and the session stays latched - under
// MOBILEGL_IPC_RECOVER=0, for a transport whose server cannot be dialed again (inproc,
// fd:), and when the server does not answer a probe. OK when a fresh session is up.
MobileGLResult RecoverAfterDeviceLoss();
// Sessions brought up in this process so far (the recovery's arm proof for a test).
static Uint64 SessionsStarted();
// While one is open on a thread, that thread's emissions are DECLINED as on a lost
// device: the lost contexts' objects die under it, and their deaths must not reach the
// fresh session.
struct ScopedWireMute {
ScopedWireMute();
~ScopedWireMute();
ScopedWireMute(const ScopedWireMute&) = delete;
ScopedWireMute& operator=(const ScopedWireMute&) = delete;
};
// While one is open on a thread, the thread's own GL state does not make it "lost": EGL
// work that targets the current session on behalf of a thread whose current context is
// a lost one (it creates the replacement context, re-creates a surface).
struct ScopedCurrentSessionWork {
ScopedCurrentSessionWork();
~ScopedCurrentSessionWork();
ScopedCurrentSessionWork(const ScopedCurrentSessionWork&) = delete;
ScopedCurrentSessionWork& operator=(const ScopedCurrentSessionWork&) = delete;
};
// The bell this session parks on, for the `dl` red-once ONLY. A test cannot otherwise
// ask the question the latch turns on - "did the peer hang up" is a fact about a
// descriptor, and the pair of tests that pin D5c has to be able to kill a server and
// then watch this object, not a side effect three layers up.
Transport::Doorbell* SelfDoorbellForTest() { return m_producer.SelfDoorbell(); }
// PH-6's fuzz arm 3 ONLY (EventForfeitPeerTest): the control stream, so a peer that has
// stopped draining can still put on it the frames no ClientSession path would send at that
// moment - a malformed one, a surface op queued behind a wait it did not drain for, a
// LogFlush every 100 ms, or a half-close. Every production path that writes control holds
// m_remoteControlMutex and drains first; a caller of this does neither, which is the
// point. Null unless spawned.
Transport::SocketTransport* ControlSocketForTest() { return m_socketTransport.get(); }
// P9 W3's fault injection F2 ONLY (EventForfeitPeerTest): the control READER, so a TCP peer
// can stop taking anything off its control connection - the server's forwarded log lines
// included - and later resume (ControlInbox::PauseReadingForTest). False when there is no
// reader to pause (not a stream session).
bool PauseControlReaderForTest(bool paused) {
if (!m_controlInbox) return false;
m_controlInbox->PauseReadingForTest(paused);
return true;
}
// CONTRACT-P6 4.3: THE PID THE SERVER STATED IN ITS Welcome, which is not the same fact
// as the pid this session's launcher recorded - that one is local knowledge, this one
// CROSSED THE WIRE. Under spawn they must agree and neither may be ours; under inproc
// the server states its own pid, which IS ours, and that is the honest answer there.
std::uint32_t PeerServerPid() const { return m_peerServerPid; }
ClientSession();
~ClientSession();
MobileGLResult AttachStreamLink(int dataFd, const Transport::SessionSegmentSizes& sizes);
void AttachDataLink(std::unique_ptr<Transport::ILink> link);
Transport::ILink* DataLink() const { return m_link.get(); }
bool ConnectDial() const { return m_connectDial; }
void SyncPeerLog();
// 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
// ran monolith and went green" failure, and it must be loud.
MobileGLResult Start(MG_Config::TransportMode mode, const String& endpoint);
// The sink ServerLoop calls when the server is another process: it encodes the frame,
// sends it and waits for the SurfaceReply carrying its seq.
// StartOverTransportPair (below) is Start()'s second half, over a pair the caller just
// made with InProcessTransport::CreatePair. It is public only so SessionHandshakeTest can
// put a frame ahead of the server's Welcome; production reaches it only through Start().
// StartSpawned and StartOverSocket run the same handshake against a server in another
// process.
static MobileGLResult RemoteControlSinkThunk(void* user, Server::SurfaceControlFrame& frame);
MobileGLResult RunRemoteSurfaceControlFrame(Server::SurfaceControlFrame& frame);
MobileGLResult FinishStartup(Transport::Doorbell* peerBell,
Transport::Doorbell* selfBell,
bool startApplyThreadHere);
// `transport` is this process's end of a connection to an ALREADY
// RUNNING server process. The two bells arrive over it, with the four
// segments, as SCM_RIGHTS offers - nothing here is inherited, because
// the two processes were started independently.
// P6: launch a server process and connect to it. The two are
// INDEPENDENT - the launcher hands over a rendezvous name and nothing
// else - so this is the same code path a client would use against a
// server that was already running when it started.
MobileGLResult StartSpawned();
MobileGLResult StartOverSocket(std::unique_ptr<Transport::SocketTransport> transport);
MobileGLResult StartOverTransportPair(std::unique_ptr<Transport::InProcessTransport> clientEnd,
std::unique_ptr<Transport::InProcessTransport> serverEnd);
// Teardown order matters and is table 3's fourth column: publish and let the server
// drain, Doorbell::Kill() (the ONLY thing that wakes an apply thread parked on
// kWaitForever, CondVarDoorbell::Kill), then join, and only then release anything an
// emitter owns - a tail still referenced by an unapplied record is a use-after-free
// the join is what prevents.
void Stop();
Wire::PipeWireEncoder& Encoder();
CapsMirror& Caps();
// Emit one record and, if the barrier is armed, wait for it. `replyOut`/`replyBytes`
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
// Returns the record's seq, which is also its reply-slot id.
//
// `replySizeOut` (optional) receives the answer's OWN byte count as the server stamped
// it - which is not always `replyBytes`: a short OK reply stamps fewer, and a DECLINE or
// ERROR stamps 0. ReadPixels is the one caller that must know, because scattering a
// reply that arrived short would spray stale bytes as pixels (M2 / codex 11); it reads
// this and refuses `replySize != DstSize` by name rather than trust the copy.
//
// Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with
// producerParked set before blocking - the shape Doorbell::Wait already implements.
Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr);
// The same call for a record that carries TWO tails (P5b d1: draw_vbo's user-index span
// or its MGPDrawIndirect block behind the range array). The one-tail form above is this
// one with a single WireTail; the barrier policy lives in exactly one body. A distinct
// name rather than an overload, because `nullptr, 0` would match both.
// P5e (ra, CONTRACT-P5E §2.5): `wantReply` is the ONE caller-side relaxation of the
// catalogue's reply rule, and it exists for exactly one row - resource_subdata's
// BUFFER half, whose Bool the only caller discards (PipeFill.cpp's
// MGPipeEmitResourceSubData). The row keeps kReplySlot on the wire (the server still
// posts; an unread slot is harmless, ReplySlotPool's seq-stamped slots), so nothing about the
// protocol moves - what moves is that under run-ahead this client neither waits for
// the answer nor reads it, which is what makes the 64 KB persistent-map push
// fire-and-forget instead of a hidden round trip per block. It is NOT a licence to
// skip an answer a caller uses: R-5 still forbids re-deriving one locally, and every
// other kReplySlot row passes true (the default) and waits.
Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const Wire::WireTail* tails, Uint32 tailCount, void* replyOut,
Uint64 replyBytes, Int32* statusOut,
Uint64* replySizeOut = nullptr, Bool wantReply = true,
Bool willReadReply = true);
// ---- P11 B2: T0 (docs/archive/contracts/CONTRACT-P11.md B2) ------------------------------------
//
// Does this session adopt persistent stores through T0? Decided ONCE, lazily, at the first
// map_persistent - the first moment it matters, and one at which the server's first native
// bind has certainly republished its caps (a buffer store cannot exist before a
// make-current, and the make-current adopts what the bind published): the Hello asked T0
// (MOBILEGL_IPC_ADOPT_TIER=0 over shared segments) AND the server published kCapAdoptT0.
// A T0 the session asked for and cannot use is T2 plus ONE named line here
// (`Refuse{AdoptT0Unavailable, ...}`), never a Fatal.
Bool AdoptT0();
// T0's map_persistent: allocate the AHB, hold its lock, seed it from `seed` (the shadow),
// queue its Offer for the record's seq BEFORE the record is published (the order the
// server's inbox relies on), and wait for the answer. The held pointer on OK - the caller
// adopts it as the store's mapping - or null on DECLINED (the store stays T2).
void* AdoptPersistentT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed);
void* AdoptPersistentInProcess(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed);
// shared_image (docs/archive/notes/anland/plan-ahb-dmabuf.md): emits `op` and waits
// for its answer. `sendFd` >= 0 (an Import) is queued on the aux socket for the record's
// seq BEFORE the record is published; `receivedFd` non-null (an Allocate) takes the
// descriptor the server queued before its answer. False = DECLINED (the server's log says
// why) or the descriptor did not travel.
Bool EmitSharedImage(const MG_Pipe::MGPSharedImageOp& op, int sendFd, MG_Pipe::MGPSharedImageReply* reply,
int* receivedFd);
// The client's reference to buffer `handle`'s T0 store ends because the buffer was
// respecified or destroyed: unlock and release it. The server's import holds its own
// reference until its own fence for the store's last use has completed. No-op for a
// handle with no T0 store.
void ReleaseT0Store(MG_Pipe::MGPipeHandle handle);
// Per-session T0 arm proof (B2): stores the server imported, their bytes, and the
// map_persistent answers that came back DECLINED in a T0 session.
Uint64 T0StoresImported() const { return m_t0Imported; }
Uint64 T0BytesImported() const { return m_t0Bytes; }
Uint64 T0Declined() const { return m_t0Declined; }
// 1 when this session's knob asked for T0 (0, or unset) and it runs T2 instead: a stream
// data plane, or no kCapAdoptT0 at the first map_persistent. Counted whether the fallback
// was named (knob 0) or quiet (unset, the default).
Uint64 T0Fallbacks() const { return m_t0Fallbacks; }
// Test seam: 0 = queue each Offer before its record (the product order); > 0 = queue it
// that many ms AFTER the record is published, from another thread; < 0 = never queue it.
// What the host tests use to prove a late Offer still lands and a lost one is a named
// DECLINED rather than a hang.
static void SetT0OfferFaultForTest(Int32 delayMs);
// EmitAndWaitTails' `wantReply` and `willReadReply` are separate questions:
// wantReply this call blocks and reads the answer before it returns;
// willReadReply someone reads the answer at all, now or later (the create window emits
// with wantReply=false and its drain reads the answers afterwards).
// Only willReadReply=false lets the server skip the answer (Transport::kRecNoReply); the
// rows in WireTables.cpp that pass it each answer their caller locally. wantReply with
// !willReadReply, and !willReadReply on a row with no reply slot, are Fatal by name: the
// first would wait for an answer never sent, the second discards one never posted.
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
// red; it must be run once and the way it goes red recorded.
Bool BarrierArmed() const;
// ---- P5e (ra): the wait rule (CONTRACT-P5E §1, §2.3, §2.4, §2.5) -------------------
// A CONJUNCTION OF THREE, LATCHED ONCE: `m_barrierArmed && Ipc.RunAhead &&
// Caps().HasCap(kCapRunAheadApply)`. The third conjunct is the server's own statement
// that it applies an unbarriered record without reading client memory, so run-ahead is
// never something the client can turn on by itself - on Magma, and on Espryt until the
// integration commit publishes bit 10, this answers false and EmitAndWaitTails runs
// today's lockstep path byte for byte.
//
// LATCHED AT THE FIRST CAPS ADOPTION AFTER Start, AND A LATER SNAPSHOT MAY ONLY TURN IT
// OFF. A caps re-run cannot promote a lockstep server to a run-ahead one mid-frame:
// records already published without a wait would have been published against the wrong
// rule, and there is no way to un-publish them. Demotion is safe in the other
// direction - it only adds waits - so it is allowed.
Bool RunAheadArmed() const;
// §2.3: the four O-class SharedPtr rows of gPipeInputs (PipeInputs' SharedPtr members - the
// bound VAO and the three programs), dropped on the GL thread once a barriered apply
// has returned. Under lockstep the next fill overwrites them within a verb; under
// run-ahead the next fill may be a frame away, and a pinned VAO whose last owner ends
// up being the apply thread is exactly the deferred-destroy hazard §2.7 keeps a queue
// for. Releasing them at the one instant the applier is provably idle costs four
// refcount drops per barriered record and removes the pin entirely.
void ReleaseFillPins();
// §2.5's forced wait, as one named entry point: publish nothing, wait for the apply
// thread to reach LastPublishedSeq, drain the reverse channel. `why` names the caller
// in the timeout Fatal, because "which forced wait wedged" is the whole diagnostic.
// A no-op when run-ahead is not armed (the client is already in step by construction).
void WaitForApplyToCatchUp(const char* why);
// P15 wait census (MOBILEGL_IPC_WAIT_STATS): the validate point names the verb whose
// barriered fill is about to quiesce the applier.
static void NoteBarrieredVerbForStats(const char* verb);
// glFinish. Flush and Finish stay no-ops ON THE WIRE (ARCHITECTURE §11, :415) - there
// is no record to emit - but "the GL commands issued so far have completed" cannot be
// true while a run-ahead queue still holds them, so under run-ahead glFinish becomes
// exactly WaitForApplyToCatchUp + the drain that comes with it.
void Finish();
// P10 (CONTRACT-P10.md §1): a polling entry point (glClientWaitSync, glGetSynciv(
// GL_SYNC_STATUS)) is a doorbell point. Everything published so far - the polled fence's
// own create record above all - is flushed to the server, and the reverse channel is
// drained, so a fence the server has since reported is known before the poll answers.
// Publishes nothing and waits for nothing.
void PollEntry();
// Submit published commands without waiting for application. This is
// glFlush's contract and the frame-submit boundary for Present.
void Flush();
// The last seq this session published, whether or not it waited for it. The forced
// waits above are defined against it.
Uint64 LastPublishedSeq() const;
// §2.4's counter, published on the stats line as `credit-waits`: how often the client
// actually blocked for a swap to come back. Zero with a credit of 1 means the server
// is never the frame's critical path; a number close to the frame count means the
// credit is what paces this workload, which is what the device exit measures.
Uint64 PresentCreditWaits() const;
// P15: how many records this session waited on (applied or reply) after publishing.
Uint64 RecordWaits() const { return m_recordWaits; }
// §2.4, called by EmitPresent immediately before it encodes: pay the credit (parking
// if this client already has `MOBILEGL_IPC_PRESENT_CREDIT` presents in flight), then
// mint and return this frame's 1-based MGPPresent::FrameSerial. The credit wait is
// BEFORE the encode on purpose: a client that encoded first would hold a SEG_CMD
// reservation across the park.
Uint64 AcquirePresentCredit();
// eglSwapBuffersWithDamage's damage for the present about to be emitted (BackendObject_Remote
// sets it around the swap; EmitPresent takes it into the record). Taking clears it, so a
// plain swap after it carries none (the whole surface).
void SetPendingPresentDamage(const MG_Util::Damage::Region& damage) {
m_pendingDamageCount = MG_Util::Damage::PackRects(damage, m_pendingDamage, MG_Pipe::kMGPMaxDamageRects);
}
Uint32 TakePendingPresentDamage(Int32* out) {
const Uint32 count = m_pendingDamageCount;
std::memcpy(out, m_pendingDamage, sizeof(Int32) * 4 * count);
m_pendingDamageCount = 0;
return count;
}
// R-1's invariant made checkable rather than only written down: true while this
// thread is inside a barrier wait. The apply thread sets its own flag on entry to the
// applier; a debug/verify build asserts the two are never both true, and that the
// client never touches gPipeInputs while the server is inside the applier.
static Bool InBarrierWait();
static Bool ApplyThreadIsInsideApplier();
// Fatal{RoleViolation, "gPipeInputs"} when a GL-thread touch of gPipeInputs could race the
// applier's reads (ARCHITECTURE.md §9.2.2). Under run-ahead only a barriered fill
// (`isBarrieredFill`) made while the applier is not inside a record is legal, so fill
// sites wait first (QuiesceApplierBeforeFill); with distinct client and server blocks a
// barriered fill is always legal. Under lockstep it checks only with
// MOBILEGL_IPC_BATCH_WAITS=0 and an armed barrier, and fires when the applier is inside
// and this thread is not in a barrier wait. Never fires on the apply thread.
static void RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface,
Bool isBarrieredFill = false);
// ---- c1's additions ---------------------------------------------------------------
// The apply thread's half of R-1's invariant. v1's apply loop brackets its
// DecodeAndApply with these; the client checks the flag before it publishes, so
// "at most one of {GL thread, apply thread} is runnable" is a runtime assertion rather
// than a sentence in a brief. A raw pair rather than an RAII type in this header
// because the server side owns its own scoping and must not have to include a client
// header to get it - ScopedApplierEntry below is the convenience, not the contract.
static void NoteApplyThreadEnteredApplier();
static void NoteApplyThreadLeftApplier();
struct ScopedApplierEntry {
ScopedApplierEntry() { NoteApplyThreadEnteredApplier(); }
~ScopedApplierEntry() { NoteApplyThreadLeftApplier(); }
ScopedApplierEntry(const ScopedApplierEntry&) = delete;
ScopedApplierEntry& operator=(const ScopedApplierEntry&) = delete;
};
// R-12's INVALIDATION EDGE. Drains whatever the server has queued on the control plane
// and adopts every CapsSnapshot in it - and a SECOND snapshot IS the invalidation,
// which is why there is no Invalidate(). Non-blocking: it peeks and returns.
//
// Called at the handshake, from BackendObject_Remote's Initialize/InitCapabilities, and
// once per Present. Present is the boundary every one of P5's three targets crosses,
// and a caps re-run can only follow a surface event, so once a frame is both sufficient
// and the cheapest place that is.
//
// Returns how many snapshots it adopted, so a case can assert the edge fired rather
// than assert that a number downstream of it happened to change.
Uint32 PumpControlPlane();
// ---- 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);
// P12: HOW MANY REPLY-OWNING RECORDS THIS CLIENT HAS EMITTED, the clock a deferred
// answer's RESIDENCY is measured against. An answer is readable only until the bounded
// reply buffer reuses its slot, so a caller that defers an answer (the create window)
// has to know how many other answers have gone by since it was posted.
//
// EVERY reply-slot row is counted, not only the rows this client waits for, and that is
// the conservative direction on purpose: a server that ignores the no-reply bit answers
// records the client does not read, and those answers displace a deferred one just the
// same. A budget that counted only "answers I will read" would be wrong in exactly the
// deployment where it matters most.
Uint64 ReplyPostings() const;
// P12: DOES THIS LINK KEEP AN ANSWER UNTIL ITS READER TAKES IT? The create window defers
// answers across an unbounded amount of other traffic, which only works where the answer
// is still there when the drain comes back - measured: a bounded buffer of the last N
// answers never is. On a link that answers false the window must not defer at all, so
// this is what CreateWindowEffective() is gated on.
Bool LinkRetainsReplies() const;
// 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. No single answer is ever bigger than this: a ReadPixels
// whose whole read is bigger is split by the emitter into row bands (or, for a row
// wider than a reply, single-row pieces) that each fit (g5-readback, EmitTables.h
// PlanReadbackBands), and the client still checks every band BEFORE it emits.
Uint32 MaxReplyBytes() const;
// ID-47, the fifth primitive: true exactly when an answer of `bytes` can be posted.
Bool ReplyCanHold(Uint64 bytes) const;
// ID-47's named refusal, forwarded verbatim to ReplySlotPool::RequireReadPixelsFits.
// Returns when the answer fits; otherwise
// Fatal{ReplyTooLarge, "ReadPixels <w>x<h> <format> <bytes> > <cap>"}
// and abort - AT THE CLIENT, BEFORE EMISSION. Package c1's OnReadPixels emitter calls
// this immediately before each EmitAndWait(MGPWireOp::ReadPixels, ...), with that
// band's box, its Format/Type enums and the DstSize it computed - and, when not even one
// pixel fits a reply, once for that one pixel instead of emitting; the server's Post
// keeps its own refusal as the last line of defence, but that one fires on the apply
// thread with the record already on the wire, where all the client sees is a hang.
void RequireReadPixelsReplyFits(Uint32 width, Uint32 height, Uint32 format, Uint32 type,
Uint64 bytes) const;
// The reverse channel's reading end: OnBufferWriteback / OnGpuWritten /
// OnSurfaceChanged. Drained by the GL thread between verbs.
Transport::EventRingConsumer& Events();
// The event ring's SECOND drain point, for the blocking EGL lifecycle RPCs
// (BackendObject_Remote's surface creation and make-current). Their return is the
// same kind of instant as EmitAndWait's post-barrier one - the apply thread is
// known idle because the RPC it was serving has completed and this thread has
// published nothing since - so the consumers' frontend writes are as legal here as
// there. A surface-changed event posted during bring-up must be applied BEFORE the
// first frontend query of the default framebuffer's depth/stencil format: waiting
// for the first verb's drain answered GL_DEPTH32F_STENCIL8 (the placeholder) for a
// depth24+stencil8 surface, and every buffer allocated from that answer was
// blit-incompatible with the real thing.
Uint32 DrainPublishedEvents();
// P12 (on-screen server window), D1: THE GEOMETRY FLOWS BACK. `surface` (the client's EGL
// handle) is this session's server-owned window surface, `width`x`height` the server
// window's real extent from the CreateWindowSurface reply (0 = not known yet). The extent
// goes into the EGL state at once - eglQuerySurface answers it before eglCreateWindowSurface
// returns - and every later surface-changed event that carries an extent (the server window
// resized or rotated) is applied to the same surface, beside the default-framebuffer
// reallocation the drain already does. Every server-owned surface is tracked (they are all
// on the server's one window, so each extent applies to all of them), and
// ForgetServerOwnedWindowSurface drops one when the client releases it.
void NoteServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
void ForgetServerOwnedWindowSurface(EGLSurface surface);
// Whether `surface` is one of them (the resize path asks: it resizes the server's window).
static Bool IsServerOwnedWindowSurface(EGLSurface surface);
// SEG_EVENT's ring capacity, exposed so the readback path can slice a writeback
// request into records that always fit (RingProducer::MaxRecordBytes ==
// capacity/2, Ring.h).
Uint64 EventRingCapacityBytes() const;
// SEG_STAGE's capacity, exposed so a content emitter can cut a range into records that
// always stage whole: the staging area is a linear arena of exactly these bytes (w1),
// and one blob larger than it is Fatal{RingOverrun, "SEG_STAGE"} at the encoder rather
// than a split (PipeWireEncoder::StageAllocate in PipeWireCodec.cpp).
Uint64 StageCapacityBytes() const;
// The CLIENT's own segment table (P5c ev, CONTRACT-P5C §4.3): the writeback
// consumer resolves a SEG_EVENT blobref through it. Never the process resolver -
// table 3 installs that one on the server role only.
Wire::SegmentTable& Segments();
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;
// R-10's maximum record bytes and R-9's wrap/wait counts, at teardown, in whatever log
// this process writes. See the definition for why it is not only on the stats line.
void LogWireLedger() const;
private:
MobileGLResult ReceiveControlFrame(std::vector<Uint8>& frame, Uint32 timeoutMs);
std::unique_ptr<Transport::ControlInbox> m_controlInbox;
std::unique_ptr<Transport::ILink> m_link;
bool m_connectDial = false;
std::uint64_t m_logFlushSeq = 0;
// P5e (ra): the latch, taken from PumpControlPlane after every adoption. Private
// because "when may run-ahead start" is this class's own rule and not a caller's.
void LatchRunAheadFromCaps();
Wire::PipeWireEncoder m_encoder;
Wire::SegmentTable m_segments;
Bool m_barrierArmed = true;
// P5e (ra): the latch of RunAheadArmed() and the fact that it has been taken once.
Bool m_runAheadArmed = false;
Bool m_runAheadLatched = false;
// §2.4: presents published (the 1-based FrameSerial space) and how often the credit
// wait really blocked.
Uint64 m_presentsSent = 0;
Uint32 m_pendingDamageCount = 0;
Int32 m_pendingDamage[MG_Pipe::kMGPMaxDamageRects * 4] = {};
Uint64 m_presentCreditWaits = 0;
Uint64 m_recordWaits = 0;
// sm: the spawn client owns ONE end and two bells. Held here rather
// than in the transport because which bell is "mine" is the session's
// knowledge, not the transport's - the same ruling as inproc's
// PeerDoorbell/SelfDoorbell split.
// cp: one control op at a time, and the client's own seq space.
std::mutex m_remoteControlMutex;
Uint64 m_remoteControlSeq = 0;
// P7 CI: false until this session's first MakeCurrent is answered ok. Until then the
// ops the server may bring its native backend up inside wait MOBILEGL_IPC_COLD_START_MS
// for their reply instead of MOBILEGL_IPC_CONTROL_TIMEOUT_MS (Config.h has both). Either
// budget bounds SILENCE (p7/spawnhang): a Wire::SurfaceProgress for the op restarts it.
// Guarded by m_remoteControlMutex, like the seq above; reset by Stop().
Bool m_serverBackendWarm = false;
// The server we launched, if we launched one. Reaped in Stop().
Server::LaunchedServer m_spawned;
std::unique_ptr<Transport::SocketTransport> m_socketTransport;
// PH-7 (4): the `tcp://` endpoint the control connection went to, for the data
// connection opened after Welcome. Empty on every other transport.
std::string m_dataEndpoint;
std::unique_ptr<Transport::Doorbell> m_socketSelfBell;
std::unique_ptr<Transport::Doorbell> m_socketPeerBell;
std::unique_ptr<Transport::InProcessTransport> m_clientTransport;
std::unique_ptr<Transport::InProcessTransport> m_serverTransport;
Transport::RingProducer* m_cmd = nullptr;
Transport::SessionProducer m_producer;
Transport::EventRingConsumer* m_events = nullptr;
Transport::ITransport* m_transport = nullptr;
Bool m_started = false;
Uint64 m_replyPostings = 0; // P12: the residency budget's clock (ReplyPostings())
// Written by whichever thread first notices the hangup - the GL thread in the barrier,
// or the event pump - and read by glGetGraphicsResetStatus on the GL thread.
std::atomic<bool> m_deviceLost{false};
// Welcome::serverPid, kept because §9.5's arm proof wants it and because a value that is
// only ever logged cannot be asserted on.
std::uint32_t m_peerServerPid = 0;
// P11 B2 (T0). The Hello's ask, settled at the handshake; the lazy decision (-1 = not
// yet, 0 = T2, 1 = T0); the stores this client holds by handle key (Gen << 32 | Slot);
// the counters; and the one-shot hook EmitAndWaitTails runs between EncodeRecord (the
// seq exists) and PublishAndNotify (the record is visible) - where the Offer is queued.
Bool m_adoptT0Asked = false;
Bool m_adoptT0Quiet = false; // the knob is unset: fallbacks at MGLOG_D
Int32 m_adoptT0Decision = -1;
std::unordered_map<Uint64, Transport::AdoptT0::HeldStore> m_t0Stores;
Uint64 m_t0Imported = 0;
Uint64 m_t0Bytes = 0;
Uint64 m_t0Declined = 0;
Uint64 m_t0Released = 0;
Uint64 m_t0Fallbacks = 0;
std::function<void(Uint64)> m_prePublish;
void ResetT0(Bool releaseStores);
// Every client-side cache keyed to the session that just ended, dropped before the next
// one serves a record (FinishStartup, on every start after the first).
void ForgetEndedSession();
};
// 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();
// F1 (P7 wave 2). Is a session bring-up running anywhere in this process right now.
//
// CapsMirror asks a narrower question through its own hook (SetCapsFirstSnapshotWait) and
// this is the half a TEST needs: a case that wants to read the mirror DURING a handshake
// has to know when the window has opened, and inferring it from a log line or a sleep is
// how such a case starts passing for the wrong reason. True from the first line of Start /
// StartSpawned / StartOverSocket / StartOverTransportPair until FinishStartup returns.
Bool BringUpInFlight();
} // namespace MobileGL::MG_Remote::Client