Files
MobileGL/MobileGL/MG_Remote/Server/PipeApplier.h
T

365 lines
22 KiB
C++

// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.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 server's applier bridge. Owner: package v1, with p1 for the stamp rule. Signatures by c0.
//
// IT IS A BRIDGE, NOT AN APPLIER. The applier already exists and is not edited by this phase:
// MG_Pipe/PipeApply.{h,cpp}, 37 MGPipeApply* free functions. This class owns the three things
// that only exist once records arrive over a wire rather than by direct call:
//
// 1. THE VERB STAMP. This is the phase's prerequisite, and it is not in the ROADMAP row.
// MGPipeApplyAccess deliberately does not stamp the poison generations
// (PipeInputs.h:612-618): "a stamp says the filler published this for THIS verb, which is
// the walk's statement, not the applier's". Under split the filler is in another role, so
// NOTHING stamps, every FilledGen[] stays 0, MGPipeInputFieldIsFresh returns false for
// everything, and a pure server aborts on the FIRST read inside SyncRenderState with
// Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"} - before reaching any
// interesting case. So: the server stamps at the verb boundary. p1 defines what is
// stamped and for which verb; v1 places the call. Neither half works alone.
//
// 2. ACCEPTANCE. Four applier entry points return Bool - ResourceCreate, ResourceRespecify,
// ResourceSubData, SetTextureParams - and MapPersistent returns void*. Those returns are
// what the CLIENT gates destructive state changes on (clearing per-level dirty flags,
// latching parameters, adopting a pointer). They go back through the reply slot, id =
// record seq (R-3), and are collected in the barrier's existing wait (R-5). The client
// may not recompute any of them.
//
// 3. R-11, THE BORROWED-POINTER RULE. A SEG_STAGE run is valid from publish until retiredSeq
// passes the record naming it. NO APPLIER ENTRY POINT MAY HOLD A POINTER PAST ITS RETURN.
// The tree has exactly one violation and it is named: GLESBufferResource::hostBytes
// (Managers.h:839), written by Ops_H_SubData (Managers.cpp:1980-1983) and Ops_H_FlushRange
// (:2035), read by six later drains (:2000, :2062, :2080, :2111, :2741, :2843). Under split
// those two must copy into server-owned storage. MOBILEGL_IPC_AUDIT=1's 0xDD fill (R-2.5)
// is the mechanical control that says whether they did.
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Ring.h"
#include "../Wire/PipeWireCodec.h"
#include <MG_Backend/Record/RecordVerbSink.h>
#include <deque>
#include <utility>
namespace MobileGL::MG_Remote::Server {
// P5e (gl, ID-111): DOES THIS SERVER PUBLISH kCapRunAheadApply? Asked of the server's OWN
// CallMask and not of a build constant - that bit IS the sentence "the client may run ahead
// of this apply", and Magma never sets it (MGPipeRunAheadCapBitsFor returns kCapNone for
// every backend but DirectGLES, ID-90). A session with no CallMask yet answers false: no
// client can have latched run-ahead against a snapshot that was never published.
//
// PUBLIC because the red-once has to be able to say "the session I built lacks bit 10" in
// its own words rather than inferring it from the stamp it is testing (ID-102's rule: the
// action under test is the probe itself).
Bool MGPipeServerPublishesRunAhead();
// ---- the barriered stamp, AS A PURE FUNCTION (ID-111) ---------------------------------
//
// ApplyOne stamps "is the client parked behind this record" before anything can ask
// (CONTRACT-P5E §2.1 / §4.4). Until the P5e integration commit the answer is `true` for
// every record whatever the wire says, because the client still blocks after each one
// (ID-103) - but the day the constant flips, the answer must ALSO ask whether this server
// published the run-ahead bit at all. It is `wireSaysBarriered` only when BOTH halves of
// the run-ahead arm are true; Magma publishes no bit 10, so a Magma server keeps stamping
// `true` and its lockstep client keeps being safe. Without the second conjunct a Magma
// server would stamp every draw_vbo / blit / clear / launch_grid UNBARRIERED (they are all
// kWaitNone) while its client is still parked, and CountBarrierPull aborts unconditionally
// on an unbarriered pull - Magma dies on its first draw, no knob involved.
//
// IT IS A PURE FUNCTION for MGPipeRunAheadCapBitsFor's reason: "Magma never runs ahead" is
// then something a unit case can hold AT THE POST-FLIP VALUE of the constant, rather than a
// claim that first becomes testable on the day the integration commit throws the switch.
constexpr Bool MGPipeApplierStampsBarriered(Bool clientWaitRuleLanded,
Bool serverPublishesRunAhead,
Bool wireSaysBarriered) {
return (clientWaitRuleLanded && serverPublishesRunAhead) ? wireSaysBarriered : true;
}
// Writes answers into SEG_REPLY at seq % slots, stamping the seq back into the slot header
// so a wrong-slot read is detectable rather than plausible (table 0's slot header row:
// {Uint64 Seq; Int32 Status; Uint32 Size;}).
class ReplyPool final : public Wire::ReplySink {
public:
ReplyPool() = default;
ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes);
void SetLink(Transport::ILink* link) { m_link = link; }
void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) override;
// A reply larger than one slot is Fatal rather than chunked: P5's only large answer is
// ReadPixels, whose size the client already knows before it emits, so the slot size is
// chosen from that and an overflow means the two sides disagree about the frame.
Uint32 SlotBytes() const;
// P12: DID THE ANSWER ACTUALLY GO OUT? PostedReplies counts the ones the link took,
// FailedReplies the ones it refused. THE PAIR EXISTS BECAUSE THE DECODER'S TALLY CANNOT
// ANSWER IT: m_answered is incremented BEFORE PostReply, so it counts ATTEMPTS. Without
// these two, a client that cannot read an answer has no way to tell "the server never
// sent it" from "it was sent and the bounded buffer dropped it" - and those two need
// opposite fixes, one in the transport and one in the deferral. (Found the hard way: a
// device run had the server report 4,521 answers and the client receive 4,501, and
// nothing in the tree could say which twenty were which.)
Uint64 PostedReplies() const { return m_posted; }
Uint64 FailedReplies() const { return m_failed; }
private:
Transport::ILink* m_link = nullptr;
Uint8* m_base = nullptr;
Uint64 m_size = 0;
Uint32 m_slots = 0;
Uint32 m_slotBytes = 0;
Uint64 m_posted = 0;
Uint64 m_failed = 0;
};
// ---- MGPClear's two discriminants ---------------------------------------------------
//
// P5b MOVED THEM INTO MGPipeTypes.h (kMGPipeClearKind* / kMGPipeClearValueClass*), which is
// where this file said they belonged: through P5 the numbers lived here and in the client's
// EmitTables.h as two hand-minted copies, and a disagreement between them is a clear of the
// wrong attachment with the wrong value type, which renders plausibly. These are ALIASES so
// v1's bodies read unchanged; new code names the MG_Pipe constants directly.
inline constexpr Uint32 kMGPClearKindWhole = MG_Pipe::kMGPipeClearKindWhole;
inline constexpr Uint32 kMGPClearKindColor = MG_Pipe::kMGPipeClearKindColor;
inline constexpr Uint32 kMGPClearKindDepth = MG_Pipe::kMGPipeClearKindDepth;
inline constexpr Uint32 kMGPClearKindStencil = MG_Pipe::kMGPipeClearKindStencil;
inline constexpr Uint32 kMGPClearKindDepthStencil = MG_Pipe::kMGPipeClearKindDepthStencil;
inline constexpr Uint32 kMGPClearValueClassFloat = MG_Pipe::kMGPipeClearValueClassFloat;
inline constexpr Uint32 kMGPClearValueClassInt = MG_Pipe::kMGPipeClearValueClassInt;
inline constexpr Uint32 kMGPClearValueClassUint = MG_Pipe::kMGPipeClearValueClassUint;
// ---- the five class-B verbs' consumer ------------------------------------------------
//
// Contract §7 class B is Clear (57), Blit (56), ReadPixels (58), DrawVbo (59) and Present
// (67), and NONE of them has an MGPipeApply* entry point - the 37 that exist are the object
// and state families. So w1's decoder validates and hands over a checked argument list and
// stops, and this is the other half: the SERVER'S OWN BACKEND CALL, through the private
// GlobalBackendFunctionsTable ServerLoop holds. It is not gBackendFunctionsTable, which in
// a split process is the client's emit table (table 3) - calling THAT here would re-emit
// the record the server is in the middle of applying, which is an infinite loop that
// renders nothing and looks like a hang.
class ServerVerbSink final : public MG_Record::RecordVerbSink {
public:
// P13 W5: the record verbs (clear, blit, readbacks, draws, dispatch, copies, images,
// XFB, mipmaps) are MG_Record::RecordVerbSink's - the record arm applies them in a
// library without a transport too. What stays here is the session's: fences and
// queries, present, the T0 map, the control records and shared images.
// P11 B2: T0's map_persistent - ServerSession::AdoptStoreT0 (it owns the aux socket).
Bool OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) override;
Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override;
Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) override;
Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) override;
Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) override;
Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) override;
void ReleaseFences();
// P10 (CONTRACT-P10.md §1): post kEventFenceSignaled for each client fence, in creation
// order, that the GPU has finished, up to the first one still pending. `flush` = the
// apply thread is going idle: submit what the driver holds first. Apply thread only.
Uint32 ReportSignaledFences(Bool flush);
// True while a created fence has not been reported: the loop then parks with a deadline
// instead of for ever, so a fence that finishes while no record arrives is still seen.
Bool HasUnreportedFences() const { return !m_unreportedFences.empty(); }
Uint64 FencesReported() const { return m_fencesReported; }
Uint64 FenceIdleFlushes() const { return m_fenceIdleFlushes; }
Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) override;
Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, Wire::QueryResultReply&) override;
Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) override;
Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) override;
void ReleaseQueries();
Bool OnPresent(const MG_Pipe::MGPPresent& present) override;
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records ----------------
//
// REAL BODIES FROM THE DAY THE ROWS EXIST, not the P5b stub shape: the rows were
// appended by the same package that lands these bodies, so there is no window in
// which a client can emit ahead of its server half.
//
// OnApplierReset (§5.1): the make-current edge's server half. ContextSerial is
// ASSERTED against this session's own count of applier_reset records - one context
// per session in P5c, so the legal sequence is 0, 1, 2, ... - and only then is
// MGPipeApplierReset() run, here, on the apply thread that owns g_applier.
// OnObjectDeath (§5.2): a null handle never crosses (the client emits nothing for an
// object its allocator cannot resolve), so one arriving is
// Fatal{ProtocolCorruption, "ObjectDeath.Handle"}; otherwise the per-kind release
// runs by the record's handle (SlotTables.h's ReleaseTwinByHandle).
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// The session's current context token becomes the record's, on the apply thread. Unlike
// OnApplierReset's serial the token is NOT an assertion about a count the two ends both
// hold - the client mints it and the server keeps the table CreateContext built - so the
// refusal here is "this session never created that token", a named ProtocolCorruption
// rather than a silent bind. Token 0 (the release) is always legal.
Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) override;
// shared_image, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): the
// session's image table (allocate / import / release) and the two GL-ordered operations
// (present the default framebuffer into an image, attach an image to a texture).
Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) override;
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
Uint64 ApplierResets() const { return m_applierResets; }
Uint64 ObjectDeaths() const { return m_objectDeaths; }
Uint64 BindContexts() const { return m_bindContexts; }
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
// assert the sequence rather than only the count.
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
// The session's per-verb tallies; the record verbs' are RecordVerbSink's.
Uint64 Presents() const { return m_presents; }
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
private:
// A backend change ends every fence and query this sink holds (they are the old
// backend's natives).
void OnBackendChanged() override;
// The wire's user-index span is checked against this session's segments.
Bool CheckUserIndices(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan& span) override {
return Wire::CheckDrawUserIndices(info, ranges, span);
}
struct FenceEntry {
Uint32 Gen = 0;
Bool Live = false;
MG_Backend::BackendSyncHandle Native = nullptr;
};
FenceEntry& FindFence(MG_Pipe::MGPipeHandle handle);
UnorderedMap<Uint32, FenceEntry> m_fences;
// P10: created and not yet reported signaled, oldest first.
std::deque<MG_Pipe::MGPipeHandle> m_unreportedFences;
Uint64 m_fencesReported = 0;
Uint64 m_fenceIdleFlushes = 0;
struct QueryEntry {
Uint32 Gen = 0;
Uint32 Kind = 0;
Bool Live = false;
Bool Active = false;
MG_Backend::BackendQueryHandle Native = nullptr;
MG_Backend::BackendSyncHandle Completion = nullptr;
};
QueryEntry& FindQuery(MG_Pipe::MGPipeHandle handle);
void EndNativeQuery(QueryEntry& entry);
Bool QueryGpuComplete(QueryEntry& entry, Bool wait);
UnorderedMap<Uint32, QueryEntry> m_queries;
Uint64 m_presents = 0;
Uint64 m_lastPresentSerial = 0;
// P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record
// and the count of accepted resets: the two are one number because the serial is the
// session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never
// dispatched on). m_objectDeaths counts records dispatched, released twin or not -
// the idempotent second path answering "nothing held it" is a legal record.
Uint64 m_applierResetSerial = 0;
Uint64 m_applierResets = 0;
Uint64 m_objectDeaths = 0;
// P14 S1. BindContexts counts the records the sink dispatched and bound on the session;
// the token itself lives on the session (ServerSession::CurrentContextToken), which is
// the object the control plane's CreateContext/DestroyContext also own - one table, one
// owner, and the sink is only the ring's door into it.
Uint64 m_bindContexts = 0;
// The newest native fence (shared_image NativeFence) this session handed out, kept so a
// later fence with nothing left to export still answers with a descriptor: by then this
// one has signaled (the backend waited the work out), and a signaled sync_file stays so.
struct OwnedFd {
int fd = -1;
OwnedFd() = default;
OwnedFd(const OwnedFd&) = delete;
OwnedFd& operator=(const OwnedFd&) = delete;
OwnedFd(OwnedFd&& other) noexcept : fd(std::exchange(other.fd, -1)) {}
OwnedFd& operator=(OwnedFd&& other) noexcept;
~OwnedFd();
void Reset(int next);
};
OwnedFd m_lastNativeFence;
};
class PipeApplier {
public:
PipeApplier() = default;
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
// Builds the decoder over the session's control page and points it at this applier's
// verb sink. Separate from the constructor because ServerSession::Accept constructs the
// applier before it has decided anything about the apply thread, and the decoder needs
// the RingControl the constructor was never given.
//
// CALLED ON THE APPLY THREAD, ONCE, BEFORE THE FIRST RECORD. PipeWireDecoder is "not
// thread safe: one decoder on the apply thread, by construction", and its constructor
// installs the process-wide apply hook.
void Attach(Transport::ILink* link, MG_Backend::BackendObject* backend);
void Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend);
void Detach();
Bool Attached() const;
// Decode one record, stamp the verb, apply, post the reply if the call has one. THE
// CALLER advances appliedSeq by exactly one, through s1's SessionConsumer::ApplyOne,
// which is that watermark's single writer; P5 FORBIDS BATCHING it (R-9), because the
// barrier's waiter reads it and a batched watermark promises work that has not run.
Bool ApplyOne(const Transport::RingRecordView& record);
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
// runs, with the verb the record belongs to.
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
// MANDATORY on leaving the applier (p1's M-5, PipeInputs.h's ServerStampedVerb block).
// The client's MGPipeValidateForVerb / MGPipeLeaveVerb also clear it, which is enough
// for inproc and NOT enough for a spawned server, where MG_Impl is not in the process:
// there the flag would latch TRUE for the server's life, every later read anywhere
// would be judged against the last verb's mask, and the sticky forwards would start
// aborting under strict on exactly the case their exemption exists for.
void LeaveApplier();
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
//
// IT FORWARDS TO MGPipeResidualPullCount() AND KEEPS NO MEMBER OF ITS OWN. The member
// c0's signature block declared is deleted rather than wired: the counter is
// process-wide in PipeInputs.cpp because the reads that increment it happen inside the
// BACKEND, arbitrarily deep under an MGPipeApply* call, with no PipeApplier in scope.
// A second copy here could only ever be a number that disagreed with the one the exit
// gate reads (p1-v1 5).
Uint64 ResidualPullCount() const;
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
// How many staged bytes the decoder has poisoned, and how many records it has applied -
// the two numbers the audit lane asserts are non-zero, since an instrumentation that
// cannot be observed to have run is decoration.
Uint64 PoisonedStageBytes() const;
Uint64 DecoderAppliedSeq() const;
ServerVerbSink& Verbs() { return m_verbs; }
const ServerVerbSink& Verbs() const { return m_verbs; }
private:
Wire::SegmentTable* m_segments = nullptr;
ReplyPool* m_replies = nullptr;
Wire::PipeWireDecoder m_decoder;
ServerVerbSink m_verbs;
Bool m_attached = false;
};
} // namespace MobileGL::MG_Remote::Server