From 418e6f50be853e0cbdf37488039c05cb20a60699 Mon Sep 17 00:00:00 2001 From: rereview Date: Fri, 11 Sep 2026 13:05:05 -0400 Subject: [PATCH] [Feat] (MG_Remote): land the P5 contract headers and their named-Fatal stubs so all seven parallel packages compile and link on day one --- MobileGL/MG_Remote/CapsCodec.cpp | 56 +++++ MobileGL/MG_Remote/CapsCodec.h | 99 +++++++++ MobileGL/MG_Remote/Client/CapsMirror.cpp | 63 ++++++ MobileGL/MG_Remote/Client/CapsMirror.h | 100 +++++++++ MobileGL/MG_Remote/Client/ClientSession.cpp | 56 +++++ MobileGL/MG_Remote/Client/ClientSession.h | 95 ++++++++ MobileGL/MG_Remote/Client/EmitTables.cpp | 47 ++++ MobileGL/MG_Remote/Client/EmitTables.h | 61 ++++++ MobileGL/MG_Remote/Server/PipeApplier.cpp | 51 +++++ MobileGL/MG_Remote/Server/PipeApplier.h | 102 +++++++++ MobileGL/MG_Remote/Server/ServerLoop.cpp | 49 +++++ MobileGL/MG_Remote/Server/ServerLoop.h | 91 ++++++++ MobileGL/MG_Remote/Server/ServerSession.cpp | 50 +++++ MobileGL/MG_Remote/Server/ServerSession.h | 72 ++++++ MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 116 ++++++++++ MobileGL/MG_Remote/Wire/PipeWireCodec.h | 230 ++++++++++++++++++++ 16 files changed, 1338 insertions(+) create mode 100644 MobileGL/MG_Remote/CapsCodec.cpp create mode 100644 MobileGL/MG_Remote/CapsCodec.h create mode 100644 MobileGL/MG_Remote/Client/CapsMirror.cpp create mode 100644 MobileGL/MG_Remote/Client/CapsMirror.h create mode 100644 MobileGL/MG_Remote/Client/ClientSession.cpp create mode 100644 MobileGL/MG_Remote/Client/ClientSession.h create mode 100644 MobileGL/MG_Remote/Client/EmitTables.cpp create mode 100644 MobileGL/MG_Remote/Client/EmitTables.h create mode 100644 MobileGL/MG_Remote/Server/PipeApplier.cpp create mode 100644 MobileGL/MG_Remote/Server/PipeApplier.h create mode 100644 MobileGL/MG_Remote/Server/ServerLoop.cpp create mode 100644 MobileGL/MG_Remote/Server/ServerLoop.h create mode 100644 MobileGL/MG_Remote/Server/ServerSession.cpp create mode 100644 MobileGL/MG_Remote/Server/ServerSession.h create mode 100644 MobileGL/MG_Remote/Wire/PipeWireCodec.cpp create mode 100644 MobileGL/MG_Remote/Wire/PipeWireCodec.h diff --git a/MobileGL/MG_Remote/CapsCodec.cpp b/MobileGL/MG_Remote/CapsCodec.cpp new file mode 100644 index 00000000..eed88646 --- /dev/null +++ b/MobileGL/MG_Remote/CapsCodec.cpp @@ -0,0 +1,56 @@ +// MobileGL - MobileGL/MG_Remote/CapsCodec.cpp +// 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 + +#include "CapsCodec.h" + +#include + +#include + +namespace MobileGL::MG_Remote { + + // The consumer mask may not collide with the MGPCapBits below it. kCapNeedsHostUboBytes + // is 1<<8 today; this asserts the gap stays a gap rather than trusting the comment. + static_assert((static_cast(MG_Pipe::kCapNeedsHostUboBytes) & kMGCapsConsumerMask) == 0, + "an MGPCapBit has grown into CallMask's consumer block (bits 32..47)"); + static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources), + MG_Pipe::kMGPipeSubsystemResources), + "the consumer encoding does not round-trip"); + static_assert(!MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources), + MG_Pipe::kMGPipeSubsystemPrograms), + "the consumer encoding answers yes for a family it was not given"); + // P4a's highest allocated subsystem bit must fit the sixteen-bit block. This is the + // assertion that turns "room to P8" from a comment into a build break. + static_assert(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a <= 0xFFFFull, + "the subsystem mask no longer fits CallMask's sixteen consumer bits"); + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedCapsCodec, \"%s\"} - P5 package w1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache&, Vector&) { + MGP5_C0_STUB("EncodeFormatCapabilities"); + } + + Bool DecodeFormatCapabilities(const void*, Uint64, MG_Backend::FormatCapabilityCache&) { + MGP5_C0_STUB("DecodeFormatCapabilities"); + } + + Bool EncodeRendererInfo(const RendererInfo&, Vector&) { MGP5_C0_STUB("EncodeRendererInfo"); } + + Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); } + + Uint64 CapsAbiFingerprint() { MGP5_C0_STUB("CapsAbiFingerprint"); } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/CapsCodec.h b/MobileGL/MG_Remote/CapsCodec.h new file mode 100644 index 00000000..a26bde20 --- /dev/null +++ b/MobileGL/MG_Remote/CapsCodec.h @@ -0,0 +1,99 @@ +// MobileGL - MobileGL/MG_Remote/CapsCodec.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 MGPCaps snapshot's serializers, and the CallMask encoding. Owner: package w1 (the two +// blob codecs) and s1 (the handshake asserts). Signatures by c0. +// +// MGPCaps (MG_Pipe/MGPipeTypes.h:126-139) has four members. Two are flat and cross by memcpy +// (DynamicBackendParameters Dynamic, Uint64 CallMask); two are MGPBlobRefs over containers +// and HAVE NO SERIALIZER IN THE TREE - the header says so itself at MGPipeTypes.h:134-136, +// "Their serializers land with the transport (P5)". They are: +// +// FormatCapabilities -> FormatCapabilityCache (MG_Backend/BackendObject.h:93-98): +// FullCaps + CaveatCaps (bitfield tables) + SampleCounts, a Vector per +// (target, format) pair. The Vector is why this cannot be a memcpy. +// RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an +// Optional, and a Vector inside GLInfo. +// +// THE ABI RULING (table 0). MGPCaps has only a COMPOSITIONAL size assertion +// (MGPipeTypes.h:145-146) because DynamicBackendParameters still carries SizeT and GLenum +// members - P0.5's fixed-width rewrite did not happen. P5 does NOT rewrite it. Instead the +// handshake asserts that both peers agree on sizeof(DynamicBackendParameters), +// sizeof(MGPCaps) and the build fingerprint, and Fatal{AbiMismatch} otherwise. P6's spawn is +// same-machine and same-binary, so it inherits this unchanged; the fixed-width rewrite is on +// P7's account. + +#pragma once +#include + +#include +#include + +namespace MobileGL::MG_Remote { + + // ---- CallMask's layout (c0's ruling, extending R-8) --------------------------------- + // + // R-8 requires the client's liveness gates - MGPipeResourceSubsystemEnabled() and + // P4aFamilyHasItsConsumer() - to answer from MGPCaps::CallMask instead of from + // MGPipeGetResourceOps(), because that op table is the SERVER's registration: under + // inproc a client reading it is right by accident, and under spawn it is null and five + // whole record families silently emit nothing. + // + // But CallMask as declared carries only the nine MGPCapBit FEATURE bits + // (MGPipeTypes.h:108-124) and has no per-family bit at all, so "read the CallMask" was + // not yet an implementable instruction. It is now: + // + // bits 0..8 MGPCapBit, unchanged. kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes + // are BOTH ZERO in P5 by ruling (table 0), which is what keeps every + // MGHostSpan out of the first IPC frame. + // bits 9..31 reserved for further MGPCapBits. + // bits 32..47 THE CONSUMER MASK: bit (32 + n) means "the server has a consumer for + // MGPipe subsystem bit n" - i.e. the server's own subsystem mask, shifted. + // Sixteen bits covers bits 0..12 allocated through P4a with room to P8. + // bits 48..63 reserved. + // + // protocol.fbs:94's `tableSlotMask: ulong` is DELETED rather than renamed (R-8 offered + // either). Two reasons, and the second is decisive: ARCHITECTURE.md:114 says CallMask + // REPLACES "is this table slot null" as the capability probe, so a field whose comment is + // "which GLFunctionsTable slots the peer registered" re-introduces exactly what it + // replaced; and GLFunctionsTable has SIXTY-NINE function-pointer slots + // (BackendObject.h:117-292), so a 64-bit mask cannot address it and never could. + inline constexpr Uint32 kMGCapsConsumerBitShift = 32; + inline constexpr Uint64 kMGCapsConsumerMask = 0xFFFFull << kMGCapsConsumerBitShift; + + // Server side: fold the subsystems this server actually consumes into a CallMask. + inline constexpr Uint64 MGCapsConsumerBits(Uint64 subsystemMask) { + return (subsystemMask & 0xFFFFull) << kMGCapsConsumerBitShift; + } + + // Client side: the ONE legal spelling of "does the server consume this family". + // `subsystemBit` is a kMGPipeSubsystem* constant (MG_Pipe/MGPipe.h), not an index. + inline constexpr Bool MGCapsServerConsumes(Uint64 callMask, Uint64 subsystemBit) { + return (callMask & MGCapsConsumerBits(subsystemBit)) != 0; + } + + // ---- the two blob codecs ------------------------------------------------------------ + // + // Byte-stable within one build; the handshake's fingerprint is what makes that enough. + // Both decoders must tolerate a truncated or over-long buffer by returning false, never + // by reading past `size`: these bytes arrive over the wire. + Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache& cache, Vector& out); + Bool DecodeFormatCapabilities(const void* bytes, Uint64 size, MG_Backend::FormatCapabilityCache& out); + + Bool EncodeRendererInfo(const RendererInfo& info, Vector& out); + Bool DecodeRendererInfo(const void* bytes, Uint64 size, RendererInfo& out); + + // ---- the ABI assertion the handshake carries ---------------------------------------- + // + // Mixes sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable) and + // the compile-time build fingerprint. Compared in Hello/Welcome; a mismatch is + // Fatal{AbiMismatch} and never a downgrade, because every alternative silently reads one + // struct as another. + Uint64 CapsAbiFingerprint(); + +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Client/CapsMirror.cpp b/MobileGL/MG_Remote/Client/CapsMirror.cpp new file mode 100644 index 00000000..136ce840 --- /dev/null +++ b/MobileGL/MG_Remote/Client/CapsMirror.cpp @@ -0,0 +1,63 @@ +// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.cpp +// 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 + +// P5 c0 stubs for package c1. Every body is MGLOG_F + std::abort and never a silent no-op: a +// caps accessor that answers a default is how a split lane runs on the wrong device's limits. + +#include "CapsMirror.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Client { + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedCapsMirror, \"%s\"} - P5 package c1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + void CapsMirror::Adopt(const MG_Pipe::MGPCaps&, const MG_Backend::FormatCapabilityCache&, + const RendererInfo&, const String&, BackendType) { + MGP5_C0_STUB("CapsMirror::Adopt"); + } + + // Not stubs: the two the placeholder contract above promises are readable before the first + // snapshot. Everything else aborts, so nothing can accidentally answer from a zeroed mirror. + Bool CapsMirror::Valid() const { return m_generation != 0; } + Uint64 CapsMirror::Generation() const { return m_generation; } + + const RendererInfo& CapsMirror::Renderer() const { MGP5_C0_STUB("CapsMirror::Renderer"); } + const MG_Backend::DynamicBackendParameters& CapsMirror::Dynamic() const { + MGP5_C0_STUB("CapsMirror::Dynamic"); + } + const MG_Backend::FormatCapabilityCache& CapsMirror::Formats() const { + MGP5_C0_STUB("CapsMirror::Formats"); + } + const String& CapsMirror::ApiVersion() const { MGP5_C0_STUB("CapsMirror::ApiVersion"); } + BackendType CapsMirror::Backend() const { MGP5_C0_STUB("CapsMirror::Backend"); } + Uint64 CapsMirror::CallMask() const { MGP5_C0_STUB("CapsMirror::CallMask"); } + Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit) const { MGP5_C0_STUB("CapsMirror::HasCap"); } + Bool CapsMirror::ServerConsumes(Uint64) const { MGP5_C0_STUB("CapsMirror::ServerConsumes"); } + Bool CapsMirror::PrefersCpuXfbPrimitiveAccounting() const { + MGP5_C0_STUB("CapsMirror::PrefersCpuXfbPrimitiveAccounting"); + } + + CapsMirror& CapsMirrorInstance() { + // ID-8: leak at exit. No frontend destructor may reach pipe or backend state from an + // exit handler, and that rule applies once per role-local singleton, not once overall. + static CapsMirror& instance = *new CapsMirror{}; + return instance; + } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/CapsMirror.h b/MobileGL/MG_Remote/Client/CapsMirror.h new file mode 100644 index 00000000..600bdc1c --- /dev/null +++ b/MobileGL/MG_Remote/Client/CapsMirror.h @@ -0,0 +1,100 @@ +// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.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's copy of the server's capabilities. Owner: package c1. Signatures by c0. +// +// WHY A MIRROR AND NOT A ROUND TRIP. There are 56 client-side caps read points +// (40 GetDynamicParameters + 7 GetRendererInfo + 4 GetFormatCapabilities + 3 GetBackendType + +// 2 GetBackendAPIVersionString), and several of them - GL_Getter.cpp:2400 and +// ShaderTranspiler/CompileEnv.cpp:120-124 - bind a reference and then read many members, so a +// partial snapshot is not an option. Every one of the 56 must be answerable locally, with no +// record on the wire. +// +// GetRendererInfo() RETURNS A REFERENCE (BackendObject.h:590), so the mirror must OWN a +// RendererInfo instance to hand back - including before the first snapshot arrives, because +// LogBackendInfo() reads it at MG_Backend/Init.cpp:21, during MG_Backend::Init(), long before +// any context exists. Ruling (scout-caps-reply ยง1.2 option (a)): the mirror answers with a +// placeholder until the first snapshot, P5 accepts one inaccurate startup log line, and +// MG_Backend::Init() is NOT restructured. +// +// GetFormatCapabilities() is NON-VIRTUAL (BackendObject.h:594), so a remote backend object +// cannot override the accessor: it must FILL BackendObject::m_formatCapabilities from this +// mirror instead. +// +// INVALIDATION IS RE-ARRIVAL (R-12). DirectGLES has no OnCapsInvalidated producer at all - it +// re-runs UpdateAdvertisedCapabilityExtensions + UpdateDynamicBackendParameters at +// BackendObject_DirectGLES.cpp:865-871 and tells the frontend nothing, which is correct in +// monolith and a silent bug under split. Rather than add a DirectGLES-side callback (a +// dev-shaped backend edit), the SERVER re-sends the whole snapshot on every InitCapabilities +// re-run and the CLIENT treats a second arrival as the invalidation. Generation() is what a +// client-side memo keys on, and it is also the re-open signal for the server-context-death +// case that MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58). + +#pragma once +#include + +#include +#include + +namespace MobileGL::MG_Remote::Client { + + class CapsMirror { + public: + // Replaces the whole mirror and bumps Generation(). Called once per CapsSnapshot, + // including the re-sends that mean "invalidate" (R-12). + void Adopt(const MG_Pipe::MGPCaps& caps, const MG_Backend::FormatCapabilityCache& formats, + const RendererInfo& renderer, const String& apiVersion, + BackendType backend); + + // False until the first snapshot. The placeholder answers below are still safe to + // read - that is the point - but a caller that can wait should. + Bool Valid() const; + + // ++ on every Adopt. A client memo that survives a server context loss must key on + // this; nothing else on the client can see that the server's context died. + Uint64 Generation() const; + + const RendererInfo& Renderer() const; + const MG_Backend::DynamicBackendParameters& Dynamic() const; + const MG_Backend::FormatCapabilityCache& Formats() const; + const String& ApiVersion() const; + // The SERVER's backend type, never a new "Remote" enumerator: frontend branches + // switch on this (GL_Framebuffer.cpp:47, GL_Texture.cpp:6536, CompileEnv.cpp:122) and + // a value they do not know silently takes the wrong arm. + BackendType Backend() const; + + Uint64 CallMask() const; + Bool HasCap(MG_Pipe::MGPCapBit bit) const; + + // R-8. `subsystemBit` is a kMGPipeSubsystem* constant. THIS IS THE ONLY LEGAL SOURCE + // of the answer on the client under split: MGPipeGetResourceOps() is the SERVER's + // registration and is null in the client process, which would silently disable the + // whole push path in the one mode that matters. + Bool ServerConsumes(Uint64 subsystemBit) const; + + // GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting (BackendObject.h:274) does NOT + // ride in MGPCaps::Dynamic - it is a member of the function table, which is precisely + // the thing a split client never receives. Its only non-test client reader is + // GL_Query.cpp:221, and under split it must be answered from kCapCpuXfbPrimitiveAccounting. + Bool PrefersCpuXfbPrimitiveAccounting() const; + + private: + MG_Pipe::MGPCaps m_caps{}; + MG_Backend::FormatCapabilityCache m_formats{}; + RendererInfo m_renderer{}; + String m_apiVersion; + BackendType m_backend = BackendType::Unknown; + Uint64 m_generation = 0; + }; + + // Per client context in principle; one per process in P5, because P5 serves one context. + // Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may + // reach pipe or backend state from an exit handler. + CapsMirror& CapsMirrorInstance(); + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp new file mode 100644 index 00000000..b800546e --- /dev/null +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -0,0 +1,56 @@ +// MobileGL - MobileGL/MG_Remote/Client/ClientSession.cpp +// 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 + +// P5 c0 stubs for packages s1 (construction, handshake) and c1 (barrier, reply read). + +#include "ClientSession.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Client { + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedClientSession, \"%s\"} - P5 packages s1/c1 have not " \ + "landed this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + // Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to + // install the remote backend object, and that question has a legitimate "no" - it is the + // monolith answer. Every call that PRESUMES a session aborts instead. + ClientSession* ClientSession::Active() { return nullptr; } + + MobileGLResult ClientSession::Start(MG_Config::TransportMode, const String&) { + MGP5_C0_STUB("ClientSession::Start"); + } + + void ClientSession::Stop() { MGP5_C0_STUB("ClientSession::Stop"); } + + Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; } + + CapsMirror& ClientSession::Caps() { return CapsMirrorInstance(); } + + Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64, + void*, Uint64, Int32*) { + MGP5_C0_STUB("ClientSession::EmitAndWait"); + } + + Bool ClientSession::BarrierArmed() const { return m_barrierArmed; } + + // False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes, + // and an assertion helper that aborts when asked is worse than useless. + Bool ClientSession::InBarrierWait() { return false; } + Bool ClientSession::ApplyThreadIsInsideApplier() { return false; } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h new file mode 100644 index 00000000..839bed71 --- /dev/null +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -0,0 +1,95 @@ +// 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, the verb barrier. Owner: package s1 +// (construction and handshake) with c1 (the barrier and the reply read). Signatures by c0. +// +// INPROC USES ShmSegment AND THE RING, NOT new[] AND NOT InProcessTransport's deques. That is +// half of what "inproc runs the same G3 codec as spawn" means: InProcessTransport +// (InProcessTransport.cpp:38-97) is two deque> plus two condvar doorbells, it +// touches neither a ring nor a codec, and building the session on top of it instead of on top +// of the ring would make the whole phase unfalsifiable. The transport supplies the two +// DOORBELLS and the control plane; the records go through SEG_CMD. +// +// THE VERB BARRIER (R-1). After emitting a verb the client blocks until +// RingControl::appliedSeq >= the seq it just got back from the encoder. It is not caution: 31 +// of the 63 PipeInputs fields are still filled by the client's residual pass out of a live +// GLContext, so an unbarriered queue lets the server read a FUTURE value of them. Two +// consequences that must be stated because both are load-bearing: +// - while the barrier holds, at most one of {GL thread, apply thread} is runnable, which is +// what makes a single process-wide gPipeInputs legal (table 3); +// - the barrier is a RETIRING object, not a design. It opens family by family as table 2's +// BARRIER-PULLED column empties, and each later package reports how many rows it left. +// +// THE BARRIER'S WAIT IS ALSO THE REPLY'S WAIT (R-3/R-5). The reply slot id IS the record seq, +// so "wait for appliedSeq >= mySeq" and "wait for my answer" are one wait and the four Bool +// acceptance returns, ReadPixels' pixels and MapPersistent's decline cost ZERO extra round +// trips. The client MUST NOT re-derive any of those four answers locally - that is the c0f/c0g +// defect P4a paid two contract corrections for, and "always accept" is ID-39's 66 lost uploads. + +#pragma once +#include + +#include +#include + +#include "../Wire/PipeWireCodec.h" +#include "CapsMirror.h" + +namespace MobileGL::MG_Remote::Client { + + class ClientSession { + public: + // Null until Start() succeeds; MG_Backend::Init() is the only caller of Start(). + static ClientSession* Active(); + + // 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); + + // 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, Doorbell.h:211-221), 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. + // + // Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with + // producerParked set before blocking - the shape Doorbell.h:121 already implements. + Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const void* varTail, Uint64 varTailBytes, void* replyOut, + Uint64 replyBytes, Int32* statusOut); + + // 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; + + // 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(); + + private: + Wire::PipeWireEncoder m_encoder; + Wire::SegmentTable m_segments; + CapsMirror* m_caps = nullptr; + Bool m_barrierArmed = true; + }; + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp new file mode 100644 index 00000000..3b8cdb03 --- /dev/null +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -0,0 +1,47 @@ +// MobileGL - MobileGL/MG_Remote/Client/EmitTables.cpp +// 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 + +// P5 c0 stubs for package c1. + +#include "EmitTables.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Client { + + // The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is + // GLFunctionsTable plus Present plus SetSwapInterval; GLFunctionsTable is 69 function + // pointers plus one Bool (PrefersCpuXfbPrimitiveAccounting, BackendObject.h:274). A slot + // added to either without a decision here is a build break, which is the point: R-4 forbids + // a null slot, so a new slot needs an owner on the day it appears. + static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) == + sizeof(MG_Backend::GLFunctionsTable) + 2 * sizeof(void (*)()), + "GlobalBackendFunctionsTable is no longer GLFunctionsTable + Present + SetSwapInterval"); + static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) == + kRemoteEmitSlotCount * sizeof(void (*)()) + sizeof(void (*)()), + "the emit table's 71 slots plus the packed Bool no longer describe the table"); + + [[noreturn]] void UnmigratedVerbFatal(const char* slot) { + // The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the + // slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is + // inert in an INFO build - and INFO is what every device lane runs. + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); + std::abort(); + } + + const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() { + MGLOG_F("MGPipe: Fatal{UnimplementedEmitTable, \"RemoteEmitTable\"} - P5 package c1 has " + "not landed this yet; c0 shipped the signature only"); + std::abort(); + } + + Uint32 ImplementedVerbCount() { return 0; } + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h new file mode 100644 index 00000000..e6641873 --- /dev/null +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -0,0 +1,61 @@ +// MobileGL - MobileGL/MG_Remote/Client/EmitTables.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's emitting function table. Owner: package c1. Signatures by c0. +// +// MG_Backend/Init.cpp:44 assigns gBackendFunctionsTable from the active backend object, and +// 91 MG_Impl/GLImpl sites call through it directly. So a BackendObject_Remote has to return a +// COMPLETE table, and "complete" is a bigger number than R-4's headline: +// +// GLFunctionsTable (BackendObject.h:117-292) = 69 function pointers +// + Bool PrefersCpuXfbPrimitiveAccounting +// GlobalBackendFunctionsTable (BackendObject.h:293-299) = the above, + Present, + SetSwapInterval +// = 71 function pointers in total +// +// R-4's rule, restated over all 71: NO SLOT MAY BE NULL, and no slot may fall through to a +// driver. A null slot is 91 potential null calls; a pass-through slot is a split lane quietly +// running monolith and going green, which is the one outcome every gate in this phase exists +// to prevent. A verb P5 does not implement gets a slot that raises +// Fatal{UnmigratedVerb, ""} - the same shape as MGPipeInputPoisonFatal, live at every +// log level, MGLOG_F + std::abort. +// +// WHICH SLOTS GET A REAL EMITTER IS DECIDED BY THE VERB CENSUS (R-4), not guessed here: +// ~/w7/notes/p5/verb-census.md. The Bool member is NOT a verb - it is answered from the caps +// mirror (kCapCpuXfbPrimitiveAccounting), see CapsMirror::PrefersCpuXfbPrimitiveAccounting. +// +// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture, +// sampler and program families do NOT come through here. They are emitted from +// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under +// split each of those becomes an encode. This table covers only the verbs - the draws, +// clears, blits, readbacks, queries, fences and present. + +#pragma once +#include + +#include + +namespace MobileGL::MG_Remote::Client { + + // The table MG_Backend::Init() installs into gBackendFunctionsTable for the remote role. + // A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8). + const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable(); + + // Called by the Fatal slots. Named separately so a death test can filter on it and so + // that the message wording lives in exactly one place. + [[noreturn]] void UnmigratedVerbFatal(const char* slot); + + // How many of the 71 slots have a real emitter. Reported at bring-up and asserted by the + // gate: a table that silently loses an emitter should not be able to look the same as one + // that never had it. + Uint32 ImplementedVerbCount(); + + // The total the count above is out of. Asserted against the struct in EmitTables.cpp, so + // a slot added to GLFunctionsTable without a decision here is a build break. + inline constexpr Uint32 kRemoteEmitSlotCount = 71; + +} // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp new file mode 100644 index 00000000..2d6c3e85 --- /dev/null +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -0,0 +1,51 @@ +// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.cpp +// 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 + +// P5 c0 stubs for package v1 (with p1 for the stamp rule). + +#include "PipeApplier.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Server { + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes) + : m_base(static_cast(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {} + + void ReplyPool::PostReply(Uint64, Int32, const void*, Uint64) { MGP5_C0_STUB("ReplyPool::PostReply"); } + + Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; } + + PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies) + : m_segments(segments), m_replies(replies) {} + + Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); } + + void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) { + MGP5_C0_STUB("PipeApplier::StampVerbBoundary"); + } + + Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; } + + void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) { + MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes"); + } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h new file mode 100644 index 00000000..48401b3c --- /dev/null +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -0,0 +1,102 @@ +// 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@"} - 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 + +#include + +#include "../Transport/Ring.h" +#include "../Wire/PipeWireCodec.h" + +namespace MobileGL::MG_Remote::Server { + + // 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 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; + + private: + Uint8* m_base = nullptr; + Uint64 m_size = 0; + Uint32 m_slots = 0; + Uint32 m_slotBytes = 0; + }; + + class PipeApplier { + public: + PipeApplier() = default; + PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies); + + // Decode one record, stamp the verb, apply, post the reply if the call has one, then + // advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): 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); + + // 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. + 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); + + private: + Wire::SegmentTable* m_segments = nullptr; + ReplyPool* m_replies = nullptr; + Wire::PipeWireDecoder m_decoder; + Uint64 m_residualPulls = 0; + }; + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp new file mode 100644 index 00000000..29ed1af8 --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -0,0 +1,49 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.cpp +// 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 + +// P5 c0 stubs for package v1 - the phase's highest-risk package. + +#include "ServerLoop.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Server { + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedServerLoop, \"%s\"} - P5 package v1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + MobileGLResult ServerLoop::Start(ServerSession&) { MGP5_C0_STUB("ServerLoop::Start"); } + + void ServerLoop::Stop() { MGP5_C0_STUB("ServerLoop::Stop"); } + + // Not a stub: teardown asks this to decide whether to Kill and join at all, and a teardown + // helper that aborts when the thread was never started is a hang in the shutdown path. + Bool ServerLoop::Running() const { return m_running; } + + MG_Backend::BackendObject* ServerLoop::Backend() { MGP5_C0_STUB("ServerLoop::Backend"); } + + MobileGLResult ServerLoop::RunOnApplyThread(ControlWork, void*) { + MGP5_C0_STUB("ServerLoop::RunOnApplyThread"); + } + + ServerLoop& ServerLoopInstance() { + // ID-8: leak at exit, like every MG_Remote singleton. + static ServerLoop& instance = *new ServerLoop{}; + return instance; + } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerLoop.h b/MobileGL/MG_Remote/Server/ServerLoop.h new file mode 100644 index 00000000..724d58ef --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerLoop.h @@ -0,0 +1,91 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.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 apply thread and the server's private backend object. Owner: package v1 - the highest +// risk item in P5. Signatures by c0. +// +// WHY THE THREAD IS THE POINT. DirectGLES has 16 IsBackendContextCurrentOnThisThread() guards +// (DirectGLES.cpp:12034..12428) and Managers.cpp has 16 CanTouchGLNow() guards (:1494..:3428); +// every one of them DEGRADES when the answer is false - fences become always-signaled, queries +// return null handles, Present creates no frame fence so the buffer pool's recycle watermark +// never advances, and the two persistent-map acquisitions (Managers.cpp:1494, :2170) DECLINE, +// which would make PersistentCoherentMapScenario unreachable. Making the apply thread the +// context owner for life turns all 32 of those answers true on the server and removes the +// whole degradation class at once. It is also exactly the shape P6's spawned server inherits. +// +// P5 BUILDS ONE THREAD, NOT TWO. No mgl-srv-io: inproc's control plane is in the same process. +// P6 splits it. +// +// PARKING AND SHUTDOWN. The thread parks on Doorbell::Wait(consumerParked, ready, spinUs, +// kWaitForever) and shuts down when Wait returns false with Dead() set. Doorbell::Kill() +// (Doorbell.h:211-221) IS THE ONLY THING that wakes a thread parked on kWaitForever - a fact +// ARCHITECTURE.md's teardown order (:537) omits and InProcessTransportTest.cpp:344 already +// pins. Kill BEFORE join; join before the client frees any emitter-owned Vector; and the join +// must be bounded (that test uses 5 s) so a regression is a red test and not a hung CI job. +// +// THE EGL OWNERSHIP MOVE. eglMakeCurrent runs ONCE on this thread and is never released +// (DirectGLES.cpp:11925 plus the six cache invalidations at :11933-11953, which become a +// one-time startup cost instead of a per-make-current storm). The client's nine EGL virtuals +// become BLOCKING control requests executed here. ReleaseEGLResources and +// ~BackendObject_DirectGLES MUST be blocking: MobileGL::Destroy() (MobileGL/Init.cpp:68) +// otherwise walks on while the server still holds the context. +// +// THE FALLBACK IS PRE-DECLARED, NOT INVENTED UNDER PRESSURE (R-1). If the context migration is +// still not running ClearThenReadPixels at the end of v1's fourth working day, the integrator - +// not the package - declares `inproc-inline`: the client thread drains the ring itself, no +// thread is created, no context migrates, and a second package picks up the thread arm. + +#pragma once +#include + +#include "ServerSession.h" + +namespace MobileGL::MG_Remote::Server { + + class ServerLoop { + public: + // Creates the apply thread, names it mgl-srv-apply, applies + // MOBILEGL_IPC_SERVER_AFFINITY (borrowing ShaderCompilePool's big-core detection) and + // LOGS THE RESOLVED MASK - an affinity that silently did nothing is indistinguishable + // from one that worked, and the split's whole performance claim rests on both halves + // landing on fast cores. + MobileGLResult Start(ServerSession& session); + + // Kill the doorbell, join the thread (bounded), then destroy the private backend object + // ON THAT THREAD before it exits. Blocking by contract - see the header note. + void Stop(); + + Bool Running() const; + + // The server role's own backend object. NOT pActiveBackendObject: that global holds the + // client's BackendObject_Remote. Table 3's ruling is that the server holds its + // BackendObject_DirectGLES privately here, and that the seven backend-internal reads of + // pActiveBackendObject - ClampSamplesToBackendSupport (BackendObject_DirectGLES.cpp:815, + // :819) and five in Utils.cpp (:74, :82, :126, :220, :260), all of them format-capability + // lookups - take the format cache as a parameter instead. That is six functions across + // two files, and it is why no thread-keyed shim is needed for MOBILEGL_BUILD_DISAGGREGATED_INPROC. + MG_Backend::BackendObject* Backend(); + + // Run one blocking control request on the apply thread and wait for it. This is how all + // nine EGL lifecycle virtuals cross; it is deliberately NOT a queue of async messages, + // because every one of them has a return value the caller acts on immediately. + // + // A raw function pointer plus a user pointer, not std::function: this runs on the + // teardown path too, and the teardown path may not allocate - ID-8's leak-at-exit rule + // exists because frontend destructors reach here from exit handlers. + using ControlWork = MobileGLResult (*)(void* user); + MobileGLResult RunOnApplyThread(ControlWork work, void* user); + + private: + ServerSession* m_session = nullptr; + Bool m_running = false; + }; + + ServerLoop& ServerLoopInstance(); + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp new file mode 100644 index 00000000..d5989d79 --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -0,0 +1,50 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerSession.cpp +// 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 + +// P5 c0 stubs for package s1. + +#include "ServerSession.h" + +#include + +#include + +namespace MobileGL::MG_Remote::Server { + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedServerSession, \"%s\"} - P5 package s1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + ServerSession* ServerSession::Active() { return nullptr; } + + MobileGLResult ServerSession::Accept(Transport::ITransport&) { MGP5_C0_STUB("ServerSession::Accept"); } + + MobileGLResult ServerSession::PublishCapsSnapshot() { + MGP5_C0_STUB("ServerSession::PublishCapsSnapshot"); + } + + Transport::RingConsumer& ServerSession::CommandRing() { return m_commands; } + Transport::RingControl& ServerSession::Control() { MGP5_C0_STUB("ServerSession::Control"); } + Wire::SegmentTable& ServerSession::Segments() { return m_segments; } + PipeApplier& ServerSession::Applier() { return m_applier; } + ReplyPool& ServerSession::Replies() { return m_replies; } + + Transport::Doorbell& ServerSession::ConsumerDoorbell() { + MGP5_C0_STUB("ServerSession::ConsumerDoorbell"); + } + Transport::Doorbell& ServerSession::ProducerDoorbell() { + MGP5_C0_STUB("ServerSession::ProducerDoorbell"); + } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h new file mode 100644 index 00000000..a2b9fda8 --- /dev/null +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -0,0 +1,72 @@ +// MobileGL - MobileGL/MG_Remote/Server/ServerSession.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 half of a session: the consumer side of the rings, the handshake reply, the caps +// snapshot. Owner: package s1. Signatures by c0. +// +// The four segment sizes are already pinned by ProtocolSmokeTest.cpp:72 and are not up for +// re-derivation here: SEG_CMD 8 MiB, SEG_STAGE 32 MiB, SEG_REPLY 8 MiB, SEG_EVENT 256 KiB. +// MOBILEGL_IPC_RING_MB and MOBILEGL_IPC_STAGE_MB move the first two; the ring caps ONE record +// at half its size, so the default 8 MiB caps a record at 4 MiB (R-10). +// +// THE TWO DOORBELL ACCESSORS ARE ON THE CONCRETE CLASS, NOT ON ITransport +// (InProcessTransport.h:64-68). P5 decides this now rather than letting P6 discover it: the +// SESSION owns the pair and hands out references, so ITransport stays the dumb control-plane +// interface its header says it is and SocketTransport does not have to grow two accessors it +// has no natural home for. Discovering this in P6 would mean re-laying one package's call sites. + +#pragma once +#include + +#include + +#include "../Transport/Doorbell.h" +#include "../Transport/ITransport.h" +#include "../Transport/Ring.h" +#include "../Wire/PipeWireCodec.h" +#include "PipeApplier.h" + +namespace MobileGL::MG_Remote::Server { + + class ServerSession { + public: + static ServerSession* Active(); + + // Maps the four segments, answers Hello with Welcome, and publishes the first + // CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is + // decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable) + // and the build fingerprint must match, and a mismatch is Fatal{AbiMismatch}. + MobileGLResult Accept(Transport::ITransport& transport); + + // Re-publishes the whole snapshot. R-12: a SECOND arrival IS the invalidation signal, + // which is how DirectGLES - which has no OnCapsInvalidated producer - tells the client + // its InitCapabilities re-ran, without any dev-shaped backend edit. It is also the + // re-open signal for "the server's ES context died and its rings were dropped", the + // event MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58). + MobileGLResult PublishCapsSnapshot(); + + Transport::RingConsumer& CommandRing(); + Transport::RingControl& Control(); + Wire::SegmentTable& Segments(); + PipeApplier& Applier(); + ReplyPool& Replies(); + + // The client rings this one; the apply thread parks on it. + Transport::Doorbell& ConsumerDoorbell(); + // The server rings this one, but only when producerParked is set (a store to a shared + // cache line otherwise burns a big core for a whole frame on a phone). + Transport::Doorbell& ProducerDoorbell(); + + private: + Transport::RingConsumer m_commands; + Wire::SegmentTable m_segments; + PipeApplier m_applier; + ReplyPool m_replies; + }; + +} // namespace MobileGL::MG_Remote::Server diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp new file mode 100644 index 00000000..4a81aee3 --- /dev/null +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -0,0 +1,116 @@ +// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +// 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 + +// P5 c0's stubs for package w1. Every body is MGLOG_F + std::abort and NOT a silent no-op: +// an unimplemented codec that returns quietly is exactly how a split lane runs monolith and +// goes green, which is the failure the whole phase is built to make impossible. + +#include "PipeWireCodec.h" + +#include +#include + +#include + +namespace MobileGL::MG_Remote::Wire { + + // Table 0's first row, mechanised: this enum and the schema's SegmentKind are ONE id + // space, and the only place they are compared is here. A schema edit that renumbers a + // segment is a build break rather than a wrong pointer on a ring. + // + // Fully qualified from the global namespace on purpose: the generated header's namespace + // is `MobileGL::Wire` and we are inside `MobileGL::MG_Remote::Wire`, so a bare `Wire::` + // resolves to THIS namespace and the assertion would silently be about the wrong enum - + // or, as it first was, fail to compile for a reason that looks unrelated. + static_assert(static_cast(::MobileGL::Wire::SegmentKind::None) == kSegNone); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Cmd) == kSegCmd); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Stage) == kSegStage); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Reply) == kSegReply); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Event) == kSegEvent); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Shadow) == kSegShadow); + static_assert(static_cast(::MobileGL::Wire::SegmentKind::Adopt) == kSegAdopt); + // And the other half of table 0's rule: MG_Pipe's "no segment" sentinel is the same 0. + static_assert(static_cast(MG_Pipe::kMGHostSpanSegNone) == kSegNone, + "kMGHostSpanSegNone and SegmentId::kSegNone must be the same value"); + +#define MGP5_C0_STUB(what) \ + do { \ + MGLOG_F("MGPipe: Fatal{UnimplementedWireCodec, \"%s\"} - P5 package w1 has not landed " \ + "this yet; c0 shipped the signature only", \ + what); \ + std::abort(); \ + } while (0) + + void SegmentTable::Install(SegmentId, SegmentView) { MGP5_C0_STUB("SegmentTable::Install"); } + + SegmentView SegmentTable::Get(SegmentId) const { MGP5_C0_STUB("SegmentTable::Get"); } + + const void* SegmentTable::Resolve(Uint32, Uint64, Uint64) const { + MGP5_C0_STUB("SegmentTable::Resolve"); + } + + void SegmentTable::InstallProcessResolver() { MGP5_C0_STUB("SegmentTable::InstallProcessResolver"); } + + void SegmentTable::UninstallProcessResolver() { + MGP5_C0_STUB("SegmentTable::UninstallProcessResolver"); + } + + // NOT a stub: the two Fatal helpers are the one thing every package needs on day one, and + // a Fatal that is itself unimplemented would report the wrong failure. + void WireProtocolFatal(const char* what, const char* detail) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : ""); + std::abort(); + } + + void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what, + static_cast(got), static_cast(expected)); + std::abort(); + } + + void CheckBlobIsHonest(MG_Pipe::MGPWireOp, const MG_Pipe::MGPBlobRef&, const SegmentTable&) { + MGP5_C0_STUB("CheckBlobIsHonest"); + } + + void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan&) { MGP5_C0_STUB("CheckHostSpanIsHonest"); } + + PipeWireEncoder::PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd, + Transport::RingProducer* stage, SegmentTable* segments) + : m_control(control), m_cmd(cmd), m_stage(stage), m_segments(segments) {} + + Bool PipeWireEncoder::Valid() const { return m_control != nullptr && m_cmd != nullptr; } + + MG_Pipe::MGPBlobRef PipeWireEncoder::StageBytes(const void*, Uint64) { + MGP5_C0_STUB("PipeWireEncoder::StageBytes"); + } + + Uint64 PipeWireEncoder::EncodeRecord(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64) { + MGP5_C0_STUB("PipeWireEncoder::EncodeRecord"); + } + + void PipeWireEncoder::Publish() { MGP5_C0_STUB("PipeWireEncoder::Publish"); } + + Uint64 PipeWireEncoder::EmitSeq() const { return m_emitSeq; } + + Uint64 PipeWireEncoder::MaxRecordBytesSeen() const { return m_maxRecordBytes; } + + PipeWireDecoder::PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments, + ReplySink* replies) + : m_control(control), m_segments(segments), m_replies(replies) {} + + Bool PipeWireDecoder::Valid() const { return m_control != nullptr && m_segments != nullptr; } + + Bool PipeWireDecoder::DecodeAndApply(const Transport::RingRecordView&) { + MGP5_C0_STUB("PipeWireDecoder::DecodeAndApply"); + } + + Uint64 PipeWireDecoder::AppliedSeq() const { return m_applySeq; } + +#undef MGP5_C0_STUB + +} // namespace MobileGL::MG_Remote::Wire diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h new file mode 100644 index 00000000..720ba6b6 --- /dev/null +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -0,0 +1,230 @@ +// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.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 + +// G3: the MGPipe record codec. Owner: package w1. +// +// This header is the CONTRACT (MG_Remote/CONTRACT-P5.md) in C++ form; P5's c0 package wrote +// it so the other seven could compile on day one against signatures that cannot then move +// under them. Every body below is a named Fatal until w1 lands the real one. +// +// WHAT THIS LAYER IS, AND WHAT IT IS NOT +// +// It turns one MGPipe call into bytes in SEG_CMD (+ SEG_STAGE), and bytes back into ONE CALL +// OF AN EXISTING MGPipeApply* FREE FUNCTION. It owns NO semantics: MG_Pipe/PipeApply.cpp is +// not edited by this package, and a decoder arm that "handles" a record itself rather than +// delegating is a review failure (R-4's rule, one level down). +// +// THE FIVE HONESTY RULES (R-2), because they are what make `inproc` worth running at all. +// In the same address space every shortcut works: MGHostSpan::Ptr dereferences, a blobref +// whose Offset is a host address resolves, and MGPipeApplyMapPersistent's return value is a +// usable pointer. So the codec is held to the SPAWN rules even when it does not need to be: +// 1. encoder writes MGHostSpan::Ptr == nullptr and points Seg/Offset at SEG_STAGE; +// 2. encoder fills a real Seg, a real in-segment Offset and a NON-ZERO Size for every +// MGPBlobRef that carries content; +// 3. decoder Fatal{ProtocolCorruption} on: Ptr != nullptr; a content record with +// Blob.Size == 0; Size != 0 with Seg == kSegNone; Offset + Size past the segment; +// 4. MGPipeApplyMapPersistent returns nullptr under split (R-6; b1's half); +// 5. with MOBILEGL_IPC_AUDIT=1 the server fills a retired record's SEG_STAGE bytes with +// 0xDD, so an implementation that kept a pointer past apply reads 0xDD next frame. +// +// SEQ. The record ordinal IS the sequence number and IS the reply-slot id (R-3): there is no +// per-record seq field on the wire (ARCHITECTURE.md:124) and no second id space. Seq is +// 1-based so that 0 can mean "nothing encoded". A kRecPad wrap filler DOES NOT ADVANCE SEQ - +// both sides must skip it before counting, or every ring wrap offsets the two sides' +// numbering permanently and nothing checksums it (R-9, Ring.h's header). + +#pragma once +#include + +#include + +#include "../Transport/Ring.h" + +namespace MobileGL::MG_Remote::Wire { + + // ---- table 0: the segment id space ------------------------------------------------- + // + // The SAME VALUES as Protocol::SegmentKind (protocol.fbs:36-44); PipeWireCodec.cpp + // static_asserts the two agree, which is the only place the flatbuffers header and this + // enum meet. 0 is ALWAYS "no segment" and is never a real segment id, which is what lets + // MGPBlobRef{Seg == 0, Size != 0} be a detectable fault rather than a legal shape. + enum SegmentId : Uint32 { + kSegNone = 0, + kSegCmd = 1, // client-owned command ring (RingControl + records) + kSegStage = 2, // client-owned bulk staging: every blob and every var-tail's bytes + kSegReply = 3, // server-owned reply pool, addressed seq % slots (R-3) + kSegEvent = 4, // server-owned event ring (the reverse channel) + kSegShadow = 5, // client-owned per-object shadow (P8+) + kSegAdopt = 6, // server-owned adopted store, client RW (P11) + }; + + // Seq is 1-based. 0 is "no record", never a valid reply-slot id. + inline constexpr Uint64 kInvalidSeq = 0; + + // One mapped segment as this ROLE sees it. Two roles in one process have two different + // SegmentTables over the same memory on purpose: a client that can resolve SEG_REPLY as + // if it owned it is the inproc cheat R-2 exists to kill. + struct SegmentView { + void* Base = nullptr; + Uint64 Size = 0; + }; + + // ---- the per-role segment table, and the process resolver hook --------------------- + // + // gMGPipeSegmentResolver (MG_Pipe/MGPipeHostSpan.h:47) is a plain non-atomic inline + // variable and there is exactly ONE of it per process, so under inproc the two roles + // cannot both install their own into it. TABLE 3's ruling: the resolver is installed by + // the SERVER role only, before the apply thread starts, and the client never resolves a + // span at all (it only ever writes Ptr = nullptr). Install() therefore takes the role. + class SegmentTable { + public: + void Install(SegmentId seg, SegmentView view); + SegmentView Get(SegmentId seg) const; + + // Bounds-checked resolve. Returns nullptr when seg is unknown, size is 0, or + // offset + size runs past the segment; the CALLER escalates that to + // Fatal{ProtocolCorruption} (R-2.3) rather than this returning into a Fatal, so a + // unit test can exercise the arithmetic without dying. + const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const; + + // Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if + // a resolver is already installed, because two roles racing on one inline variable is + // the failure this function exists to make loud. + void InstallProcessResolver(); + static void UninstallProcessResolver(); + + private: + SegmentView m_views[kSegAdopt + 1]; + }; + + // ---- the four Fatal arms, worded once ---------------------------------------------- + // + // One function so encoder, decoder and every package's own bounds check produce the SAME + // log line. `what` is the record or field; `detail` is the number that was wrong. + [[noreturn]] void WireProtocolFatal(const char* what, const char* detail); + [[noreturn]] void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected); + + // R-2.3 arms 1-4 over one record's blobref. Split only; a monolith emission is exempt by + // construction because it never reaches this layer. + void CheckBlobIsHonest(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob, + const SegmentTable& segments); + // R-2.3 arm for MGHostSpan. P5's reduced path should produce ZERO host spans + // (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are both 0 in P5, table 0), so this + // firing at all is a finding, not just a corruption check. + void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span); + + // ---- encoder ----------------------------------------------------------------------- + // + // Not thread safe: one encoder per client context, driven by the GL thread, by + // construction (SPSC is the ring's contract too). + class PipeWireEncoder { + public: + PipeWireEncoder() = default; + PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd, + Transport::RingProducer* stage, SegmentTable* segments); + + Bool Valid() const; + + // Copies `size` bytes into SEG_STAGE and returns the blobref that names them: + // {Seg = kSegStage, Offset = in-segment byte offset, Size = size}. R-2.2 - Size is + // NEVER 0 for a content blob, and a 0-size call is a programming error that Fatals + // rather than returning an empty ref, because "the record declared no blob" and "the + // record declared an empty blob" must not be spelled the same way on a wire. + // + // The bytes are valid until retiredSeq passes the record that names them (R-11). + MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size); + + // Writes one record: header (op, MGPipeCallFlagsFor(op), total size), then the fixed + // payload, then the variable tail. Returns the record's SEQ, which is also its + // reply-slot id (R-3), or kInvalidSeq if the ring refused it. + // + // A record larger than RingProducer::MaxRecordBytes() is Fatal{RingOverrun}, NOT a + // wait: R-10 says P5 does no chunking and must instead PROVE it never needs any, so + // this is where the proof fails loudly if it is wrong. MaxRecordBytesSeen() is the + // counter that feeds that proof into MEASUREMENTS. + Uint64 EncodeRecord(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const void* varTail = nullptr, Uint64 varTailBytes = 0); + + // Release-stores the head cursor, then rings the consumer doorbell IF PARKED. The + // order is pinned by RingTest.cpp:446 and must not be swapped: notify-then-publish + // loses the wakeup. + void Publish(); + + // The highest seq this encoder has produced. The verb barrier (R-1) waits for + // RingControl::appliedSeq to reach it. + Uint64 EmitSeq() const; + + // R-10's proof obligation: the largest single record this encoder has written. + Uint64 MaxRecordBytesSeen() const; + + private: + Transport::RingControl* m_control = nullptr; + Transport::RingProducer* m_cmd = nullptr; + Transport::RingProducer* m_stage = nullptr; + SegmentTable* m_segments = nullptr; + Uint64 m_emitSeq = kInvalidSeq; + Uint64 m_maxRecordBytes = 0; + }; + + // ---- decoder ----------------------------------------------------------------------- + + // Where a kReplySlot answer goes. Declared HERE and not in Server/ so the codec does not + // depend on the server session: the decoder's job ends at "produce the answer bytes". + // + // The slot is addressed seq % slots and the server writes the seq back into the slot + // header for self-check (table 0's slot header row). 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 say false (R-5). + class ReplySink { + public: + virtual ~ReplySink() = default; + static constexpr Int32 kStatusOk = 0; + static constexpr Int32 kStatusDeclined = 1; + static constexpr Int32 kStatusError = 2; + virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0; + }; + + // Not thread safe: one decoder on the apply thread, by construction. + class PipeWireDecoder { + public: + PipeWireDecoder() = default; + PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments, + ReplySink* replies); + + Bool Valid() const; + + // Decodes ONE record and calls the matching MGPipeApply* free function. + // + // TWO BOUNDS CHECKS, NOT ONE. The generated MGP_WIRE_CHECK_BOUNDS only proves + // `size >= sizeof(MGPWireRec_X)` - IT CANNOT SEE THE TAIL, so a record declaring + // Count = 4000 while carrying 8 bytes passes it today. The decoder must recompute the + // total from the declared count(s) and require it to EQUAL MGPWireRecHeader::Size. + // The three double-tailed shapes are SetShaderBuffers (MGPBufferRange[Count] then + // MGHostSpan[HostSpanCount]), SetStreamOutputTargets (MGPBufferRange[Count] then + // Uint32[Count]) and DrawVbo (MGPDrawRange[NumDraws] then a conditional MGHostSpan). + // + // Returns whether the record was applied. False is reserved for a record this build + // deliberately does not implement; a MALFORMED record never returns, it Fatals. + // + // A kRecPad record must be skipped by the CALLER before this is reached; passing one + // here Fatals, because a pad that reached the decoder has already been counted. + Bool DecodeAndApply(const Transport::RingRecordView& record); + + // Advanced by exactly one per applied non-pad record. P5 FORBIDS BATCHING IT (R-9): + // the verb barrier's waiter reads it, and a batched watermark makes the client wait + // for records the server has not run. + Uint64 AppliedSeq() const; + + private: + Transport::RingControl* m_control = nullptr; + SegmentTable* m_segments = nullptr; + ReplySink* m_replies = nullptr; + Uint64 m_applySeq = kInvalidSeq; + }; + +} // namespace MobileGL::MG_Remote::Wire