mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Feat] (MG_Remote): land the P5 contract headers and their named-Fatal stubs so all seven parallel packages compile and link on day one
This commit is contained in:
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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<Uint64>(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<Uint8>&) {
|
||||||
|
MGP5_C0_STUB("EncodeFormatCapabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool DecodeFormatCapabilities(const void*, Uint64, MG_Backend::FormatCapabilityCache&) {
|
||||||
|
MGP5_C0_STUB("DecodeFormatCapabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool EncodeRendererInfo(const RendererInfo&, Vector<Uint8>&) { 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
|
||||||
@@ -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<Int> per
|
||||||
|
// (target, format) pair. The Vector is why this cannot be a memcpy.
|
||||||
|
// RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an
|
||||||
|
// Optional<String>, and a Vector<GLExtension> 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 <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
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<Uint8>& out);
|
||||||
|
Bool DecodeFormatCapabilities(const void* bytes, Uint64 size, MG_Backend::FormatCapabilityCache& out);
|
||||||
|
|
||||||
|
Bool EncodeRendererInfo(const RendererInfo& info, Vector<Uint8>& 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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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<vector<uint8_t>> 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 <Includes.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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, "<slot>"} - 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 <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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<Uint8*>(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
|
||||||
@@ -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@<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_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <Includes.h>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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 <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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 <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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<Uint32>(::MobileGL::Wire::SegmentKind::None) == kSegNone);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Cmd) == kSegCmd);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Stage) == kSegStage);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Reply) == kSegReply);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Event) == kSegEvent);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Shadow) == kSegShadow);
|
||||||
|
static_assert(static_cast<Uint32>(::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<Uint32>(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<unsigned long long>(got), static_cast<unsigned long long>(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
|
||||||
@@ -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 <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#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
|
||||||
Reference in New Issue
Block a user