Files
MobileGL/MobileGL/MG_Pipe/PipeRoute.cpp
T

408 lines
22 KiB
C++

// MobileGL - MobileGL/MG_Pipe/PipeRoute.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
// The MONOLITH arm of R-17's routing: thirty-eight adapters that unpack a generated table
// row's parameters and call the MGPipeApply* entry point the call site used to call directly.
// Owner: package c1. See PipeRoute.h for why the arm exists and what R-17 actually cost.
//
// G2 IS THE WHOLE SPECIFICATION OF THIS FILE. Under monolith transport the thunks must reach
// EXACTLY the code they reach today - so every adapter below is a parameter shuffle and
// nothing else. There is no branch, no cache, no early return and no logging on any of them,
// because each of those is a way for `MOBILEGL_TRANSPORT=monolith` to stop being the control
// arm that the split arm is measured against. The one thing an adapter may do beyond calling
// through is POST A REPLY, and only the five rows whose call site consumes a value do that.
//
// WHO CALLS THE INSTALL, AND WHY IT IS NOT IN THIS FILE. `gMGPipeScreen` and `gMGPipeContext`
// are inline variables with CONSTANT initialisation, so they are zero - every entry null,
// which is the pre-migration state - before any dynamic initialiser runs. The installer is
// driven from an inline variable in PipeRoute.h rather than from a static initialiser here,
// and the comment beside `detail::gMonolithTablesInstalled` says why: a static initialiser in
// THIS file is only in the program if THIS object file is in the link, and over a static
// archive it was not. That makes "the tables are installed" an invariant rather than a step
// someone can forget, and it is why no MGP_* thunk needs a null check - which matters, because
// a null check on a table slot is precisely the shape CONTRACT-P5 §7 spends the 41 call sites
// killing.
#include "PipeRoute.h"
#include <MG_Util/Debug/Log.h>
#include <Config.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Pipe {
namespace {
// ---------------------------------------------------------------------------------
// The reply mailbox
// ---------------------------------------------------------------------------------
struct ReplyMailbox {
Uint64 Slot = 0; // the MGPReplySlot::Id this answer belongs to; 0 = empty
Int32 Status = 0; // 0 OK, 1 DECLINED, 2 ERROR (Transport::ReplyStatus)
Uint64 Value = 0; // the Bool, or the pointer, the row answered with
Uint64 NextTicket = 0;
Uint64 Taken = 0;
Uint64 Declined = 0;
};
// THREAD-LOCAL, and one entry deep, because R-1's verb barrier makes the in-flight
// depth exactly one. A second post before the first is taken is not a capacity problem:
// it is a barrier that has stopped holding, and it Fatals rather than overwriting.
thread_local ReplyMailbox g_reply;
MGPipeRouteArm g_arm = MGPipeRouteArm::kNone;
// The monolith adapters, kept as tables of their own so the split arm can reach them
// on the server role's thread. See PipeRoute.h.
MGPipeScreen g_monolithScreen{};
MGPipeContext g_monolithContext{};
MGPipeRouteEscapes g_monolithEscapes{};
} // namespace
const MGPipeScreen& MGPipeMonolithScreen() { return g_monolithScreen; }
const MGPipeContext& MGPipeMonolithContext() { return g_monolithContext; }
const MGPipeRouteEscapes& MGPipeMonolithEscapes() { return g_monolithEscapes; }
MGPReplySlot MGPipeMintReplySlot() {
// 1-based, for ReplySlot.h's reason: "Seq is 1-based; 0 means 'no record'". A zero id
// must stay unmintable so an un-posted mailbox is distinguishable from a posted one.
return MGPReplySlot{++g_reply.NextTicket};
}
void MGPipePostReply(const MGPReplySlot& slot, Int32 status, Uint64 value) {
if (slot.Id == 0) {
MGLOG_F("MGPipe: Fatal{ReplyToNoSlot} - a table row posted an answer against reply "
"slot 0, which ReplySlot.h reserves for \"no record\"");
std::abort();
}
if (g_reply.Slot != 0 && g_reply.Slot != slot.Id) {
MGLOG_F("MGPipe: Fatal{ReplyOverrun} - slot %llu posted while slot %llu was still "
"unread. The mailbox is one deep because R-1's verb barrier makes the "
"in-flight depth one; two outstanding answers means the barrier is not "
"holding",
static_cast<unsigned long long>(slot.Id),
static_cast<unsigned long long>(g_reply.Slot));
std::abort();
}
g_reply.Slot = slot.Id;
g_reply.Status = status;
g_reply.Value = value;
}
namespace {
// The one reader. Fatals rather than defaulting, in both directions - see PipeRoute.h.
Uint64 TakeReply(const MGPReplySlot& slot, const char* row, Int32* statusOut) {
if (g_reply.Slot == 0) {
MGLOG_F("MGPipe: Fatal{ReplyMissing, \"%s\"} - the row was routed and nothing "
"posted an answer. R-5 forbids re-deriving acceptance on the client and "
"forbids assuming it, so there is no default to fall back to",
row);
std::abort();
}
if (g_reply.Slot != slot.Id) {
MGLOG_F("MGPipe: Fatal{ReplyMismatched, \"%s\"} - waiting on slot %llu, the "
"mailbox holds slot %llu",
row, static_cast<unsigned long long>(slot.Id),
static_cast<unsigned long long>(g_reply.Slot));
std::abort();
}
const Uint64 value = g_reply.Value;
if (statusOut != nullptr) *statusOut = g_reply.Status;
if (g_reply.Status == 1) ++g_reply.Declined;
++g_reply.Taken;
g_reply.Slot = 0;
g_reply.Value = 0;
g_reply.Status = 0;
return value;
}
} // namespace
Bool MGPipeTakeReplyBool(const MGPReplySlot& slot, const char* row) {
Int32 status = 0;
const Uint64 value = TakeReply(slot, row, &status);
// DECLINED IS A REAL ANSWER AND IT IS `false`, not a failure (ReplySlot.h). ERROR is
// not an acceptance answer at all and may not be folded into either.
if (status == 2) {
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"} - the row answered ERROR, which is not an "
"acceptance answer; folding it into accepted or refused would make a "
"transport fault look like a resource decision",
row);
std::abort();
}
if (status == 1) return false;
return value != 0;
}
void* MGPipeTakeReplyPointer(const MGPReplySlot& slot, const char* row) {
Int32 status = 0;
const Uint64 value = TakeReply(slot, row, &status);
if (status == 2) {
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"}", row);
std::abort();
}
// DECLINED is the null pointer, and it is R-6's answer for map_persistent under split.
if (status == 1) return nullptr;
return reinterpret_cast<void*>(static_cast<std::uintptr_t>(value));
}
Uint64 MGPipeRepliesTaken() { return g_reply.Taken; }
Uint64 MGPipeRepliesDeclined() { return g_reply.Declined; }
MGPipeRouteArm MGPipeInstalledArm() { return g_arm; }
void MGPipeNoteInstalledArm(MGPipeRouteArm arm) { g_arm = arm; }
Bool MGPipeTablesAreInstalled() { return g_arm != MGPipeRouteArm::kNone; }
// ---------------------------------------------------------------------------------
// The thirty-eight monolith adapters
// ---------------------------------------------------------------------------------
//
// Three shapes, so the eye can check them against PipeTables.inc in one pass rather than
// reading thirty-eight bodies. A row that does not fit one of the three is written out by
// hand BELOW the macros, never by widening a macro - a macro that grew a special case is
// how one of these silently stops being a parameter shuffle.
namespace {
#define MGP_MONO_PLAIN(Name, Payload) \
void Mono_##Name(const Payload* payload) { MGPipeApply##Name(*payload); }
#define MGP_MONO_BLOB(Name, Payload) \
void Mono_##Name(const Payload* payload, const void* blobBytes, Uint64) { \
MGPipeApply##Name(*payload, blobBytes); \
}
#define MGP_MONO_TAIL(Name, Payload, TailType) \
void Mono_##Name(const Payload* payload, const void* varTail, Uint32) { \
MGPipeApply##Name(*payload, static_cast<const TailType*>(varTail)); \
}
// -- screen -------------------------------------------------------------------
MGP_MONO_PLAIN(ResourceDestroy, MGPHandleOnly)
MGP_MONO_PLAIN(UnmapPersistent, MGPHandleOnly)
// -- context, plain -----------------------------------------------------------
MGP_MONO_PLAIN(BindRenderState, MGPBindRenderState)
MGP_MONO_PLAIN(DeleteRenderState, MGPHandleOnly)
MGP_MONO_PLAIN(BindVertexElements, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteVertexElements, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteSamplerState, MGPHandleOnly)
MGP_MONO_PLAIN(CreateSamplerView, MGPSamplerView)
MGP_MONO_PLAIN(DeleteSamplerView, MGPHandleOnly)
MGP_MONO_PLAIN(BindShaderState, MGPHandleOnly)
MGP_MONO_PLAIN(DeleteShaderState, MGPHandleOnly)
MGP_MONO_PLAIN(SetDrawProgram, MGPHandleOnly)
MGP_MONO_PLAIN(SetDispatchProgram, MGPHandleOnly)
MGP_MONO_PLAIN(SetFramebufferState, MGPFramebufferState)
MGP_MONO_PLAIN(SetIndexBuffer, MGPIndexBuffer)
MGP_MONO_PLAIN(SetPixelPackState, MGPPixelPackState)
MGP_MONO_PLAIN(SetPatchState, MGPPatchState)
// P5c (rv): dormant under monolith - PipeFill.cpp's producer is transport-gated, so
// nothing routes here without a live wire. It exists because the row has an
// MGPipeApply* entry point (unlike the two control records) and because the wire
// emitter's server-role arm forwards to this table (WireTables.cpp).
MGP_MONO_PLAIN(SetContextValues, MGPContextValues)
// -- context, blob companion --------------------------------------------------
MGP_MONO_BLOB(CreateRenderState, MGPRenderStateDesc)
MGP_MONO_BLOB(CreateVertexElements, MGPVertexElements)
MGP_MONO_BLOB(SetDynamicState, MGPDynamicState)
MGP_MONO_BLOB(SetGlobalConstants, MGPGlobalConstants)
MGP_MONO_BLOB(BufferSubDataResident, MGPSubData)
// -- context, variable tail ---------------------------------------------------
MGP_MONO_TAIL(SetVertexBuffers, MGPVertexBuffers, MGPVertexBuffer)
MGP_MONO_TAIL(SetSamplerViews, MGPSamplerViews, MGPBoundView)
MGP_MONO_TAIL(BindSamplerStates, MGPSamplerStates, MGPipeHandle)
MGP_MONO_TAIL(SetShaderImages, MGPShaderImages, MGPImageView)
// P5e (sb, CONTRACT-P5E.md §5.6). Catalogued since P4a with no producer and no
// consumer; the adapter lands with the applier's body. The generic tail shape fits
// because the SECOND tail is absent on every Espryt configuration (HostSpanCount is 0
// while kCapNeedsHostUboBytes is 0, which is the whole of P5).
MGP_MONO_TAIL(SetShaderBuffers, MGPShaderBuffers, MGPBufferRange)
MGP_MONO_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
#undef MGP_MONO_PLAIN
#undef MGP_MONO_BLOB
#undef MGP_MONO_TAIL
// -- the rows that fit none of the three shapes --------------------------------
// create_sampler_state: the blob is a TYPED frontend struct, not bytes the applier
// reads through a void*. The cast is the whole adapter, and it is exact: the client
// stages sizeof(SamplerParameters) bytes of the same object (CONTRACT-P5 table 1
// row 17, including its padding trap - the bytes staged must be the bytes a later
// memcmp compares).
void Mono_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64) {
MGPipeApplyCreateSamplerState(*payload,
static_cast<const SamplerParameters*>(blobBytes));
}
// set_residual_value_state: RETIRED (P13 W3c). Nothing emits it any more; the row stays
// installed so the table is total, and reaching it is a named death.
void Mono_SetResidualValueState(const MGPResidualValueState*, const void*, Uint64) {
MGLOG_F("MGPipe: Fatal{ResidualBlockSize} - set_residual_value_state reached the monolith "
"route, but %s", "the residual value block was deleted in P13 W3c (op 46 is a retired row; the verify comparator owns the capability cross-check)");
std::abort();
}
// resource_readback carries kReplySlot but its answer is COMPLETION, not a value: the
// bytes go server -> client through SEG_EVENT's OnBufferWriteback (CONTRACT-P5 table 1
// row 22), and the split decoder posts exactly kStatusOk with a zero-length payload.
// The monolith arm says the same thing, so the two arms hand their caller the same
// answer rather than one of them handing it nothing.
void Mono_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
MGPipeApplyResourceReadback(*payload);
MGPipePostReply(*reply, 0, 0);
}
// The three acceptance rows that DO fit a generated signature. Each posts the
// applier's own answer; none of them invents one.
void Mono_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplyResourceCreate(*payload);
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
void Mono_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplySetTextureParams(*payload);
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
void Mono_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64,
const void* varTail, Uint32, MGPReplySlot* reply) {
const Bool accepted = MGPipeApplyResourceSubData(
*payload, blobBytes, static_cast<const MGPSubRegion*>(varTail));
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
}
// -- the four escapes ----------------------------------------------------------
Bool Mono_Escape_ResourceRespecify(const MGPResourceDesc* desc, const void* initialBytes,
const MGPRespecifiedLevel* level) {
return MGPipeApplyResourceRespecify(*desc, initialBytes, level);
}
void Mono_Escape_ResourceFlushRange(const MGPFlushRange* record, const void* bytes) {
MGPipeApplyResourceFlushRange(*record, bytes);
}
void* Mono_Escape_MapPersistent(const MGPHandleOnly* handle, Uint64 size,
const void* seedBytes) {
return MGPipeApplyMapPersistent(*handle, size, seedBytes);
}
void Mono_Escape_CreateShaderState(const MGPProgramDesc* desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv,
const Uint32* linkedStages, Uint32 linkedStageCount) {
// The stage list is the CLIENT ARM's framing input and this arm has no use for it:
// the monolith twin reads GetLinkedShaderStages() off the frontend object it is
// handed. Named and discarded rather than left out of the signature, so the two
// arms stay one row.
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4c: MONOLITH'S RECORD ARM READS THE RECORD'S ARCHIVE (ProgramHandleArm), so it
// gets one: the link's snapshot, taken here, once per link - the same thing the wire
// decodes into the record, without the codec. A copy rather than a borrow, because the
// record is a snapshot of THIS link and the frontend object's artefacts are replaced
// by the next one while the record may still be in use.
if (MG_Config::RecordArmAliasesFrontend() && link != nullptr && spirv != nullptr) {
auto archive = MakeShared<MG_State::GLState::ProgramArchive>();
archive->Link = *link;
archive->Spirv = *spirv;
if (linkedStages != nullptr) archive->LinkedStages.assign(linkedStages, linkedStages + linkedStageCount);
MGPipeApplyCreateShaderState(*desc, link, spirv, std::move(archive));
return;
}
#endif
(void)linkedStages;
(void)linkedStageCount;
// P5e (pg): NO ARCHIVE ON THIS ARM, and the null is the arm selection rather than a
// hole. Under monolith the two companion pointers ARE the frontend's own archive,
// the backend twin reads it through its frontend overload (ruling 1 keeps that arm
// token for token), and serialising here to deserialise into the record would put
// EncodeProgramArtifacts on the monolith path - which PipeRoute.h's escape note
// and PipeApply.h both promise it is not.
MGPipeApplyCreateShaderState(*desc, link, spirv, nullptr);
}
// P5e (pg), the fifth escape. A pass-through like the four above: the caller already
// holds the three tails and the parallel name array, and under monolith there is no
// segment to resolve - the names ARE the frontend's own String::c_str()s, alive for
// the duration of the call, which is exactly the lifetime the applier's copy-in needs.
void Mono_Escape_SetProgramBindings(const MGPProgramBindings* hdr, const Int32* blockBindings,
const MGPProgramSamplerUnit* samplerUnits,
const MGPProgramStorageOverride* storageOverrides,
const char* const* storageOverrideNames) {
MGPipeApplySetProgramBindings(*hdr, blockBindings, samplerUnits, storageOverrides,
storageOverrideNames);
}
} // namespace
void MGPipeInstallMonolithTables() {
gMGPipeScreen.ResourceCreate = &Mono_ResourceCreate;
gMGPipeScreen.ResourceDestroy = &Mono_ResourceDestroy;
gMGPipeScreen.UnmapPersistent = &Mono_UnmapPersistent;
gMGPipeContext.CreateRenderState = &Mono_CreateRenderState;
gMGPipeContext.BindRenderState = &Mono_BindRenderState;
gMGPipeContext.DeleteRenderState = &Mono_DeleteRenderState;
gMGPipeContext.CreateVertexElements = &Mono_CreateVertexElements;
gMGPipeContext.BindVertexElements = &Mono_BindVertexElements;
gMGPipeContext.DeleteVertexElements = &Mono_DeleteVertexElements;
gMGPipeContext.CreateSamplerState = &Mono_CreateSamplerState;
gMGPipeContext.DeleteSamplerState = &Mono_DeleteSamplerState;
gMGPipeContext.CreateSamplerView = &Mono_CreateSamplerView;
gMGPipeContext.DeleteSamplerView = &Mono_DeleteSamplerView;
gMGPipeContext.BindShaderState = &Mono_BindShaderState;
gMGPipeContext.DeleteShaderState = &Mono_DeleteShaderState;
gMGPipeContext.SetDrawProgram = &Mono_SetDrawProgram;
gMGPipeContext.SetDispatchProgram = &Mono_SetDispatchProgram;
gMGPipeContext.SetDynamicState = &Mono_SetDynamicState;
gMGPipeContext.SetFramebufferState = &Mono_SetFramebufferState;
gMGPipeContext.SetVertexBuffers = &Mono_SetVertexBuffers;
gMGPipeContext.SetIndexBuffer = &Mono_SetIndexBuffer;
gMGPipeContext.SetSamplerViews = &Mono_SetSamplerViews;
gMGPipeContext.BindSamplerStates = &Mono_BindSamplerStates;
gMGPipeContext.SetShaderImages = &Mono_SetShaderImages;
gMGPipeContext.SetShaderBuffers = &Mono_SetShaderBuffers;
gMGPipeContext.SetGlobalConstants = &Mono_SetGlobalConstants;
gMGPipeContext.SetVertexAttribDefaults = &Mono_SetVertexAttribDefaults;
gMGPipeContext.SetPixelPackState = &Mono_SetPixelPackState;
gMGPipeContext.SetPatchState = &Mono_SetPatchState;
gMGPipeContext.SetContextValues = &Mono_SetContextValues;
gMGPipeContext.SetResidualValueState = &Mono_SetResidualValueState;
gMGPipeContext.SetTextureParams = &Mono_SetTextureParams;
gMGPipeContext.ResourceSubData = &Mono_ResourceSubData;
gMGPipeContext.BufferSubDataResident = &Mono_BufferSubDataResident;
gMGPipeContext.ResourceReadback = &Mono_ResourceReadback;
gMGPipeRouteEscapes.ResourceRespecify = &Mono_Escape_ResourceRespecify;
gMGPipeRouteEscapes.ResourceFlushRange = &Mono_Escape_ResourceFlushRange;
gMGPipeRouteEscapes.MapPersistent = &Mono_Escape_MapPersistent;
gMGPipeRouteEscapes.CreateShaderState = &Mono_Escape_CreateShaderState;
gMGPipeRouteEscapes.SetProgramBindings = &Mono_Escape_SetProgramBindings;
// KEPT, not merely installed. The client arm overwrites the three tables above; these
// three copies are what it forwards to when it finds itself on the server's own thread.
g_monolithScreen = gMGPipeScreen;
g_monolithContext = gMGPipeContext;
g_monolithEscapes = gMGPipeRouteEscapes;
MGPipeNoteInstalledArm(MGPipeRouteArm::kMonolith);
}
} // namespace MobileGL::MG_Pipe