Files
MobileGL/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
T
swung0x48 4ed1c9cef2 [Feature] (P14): state ownership - multi-session, multi-context, share groups
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.

S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.

S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.

S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.

S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.

S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.

Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.

Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
2026-10-01 19:16:41 -04:00

2600 lines
153 KiB
C++

// 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 package w1: the G3 record codec.
//
// ONE CALL IN, BYTES OUT; BYTES IN, ONE MGPipeApply* CALL OUT. This file owns NO semantics -
// MG_Pipe/PipeApply.cpp is not edited by this package and every decoder arm ends in an
// existing free function or in the verb sink v1 installs. An arm that "handles" a record
// itself rather than delegating is a review failure (R-4's rule, one level down).
//
// THE THING THAT MOST NEARLY WENT WRONG, WRITTEN AT THE TOP BECAUSE IT IS INVISIBLE:
// MGPWireRecHeader::Flags and Transport::RingRecordHeader::flags ARE THE SAME 16-BIT FIELD,
// and the two flag spaces COLLIDE. MGPipeCallFlags::kVarTail is 1<<2 and
// RingRecordFlags::kRecPad is 1<<2, so an encoder that stamped MGPipeCallFlagsFor(op) into
// the header - which the generated comment ("MGPipeCallFlags of the call") invites - would
// make RingConsumer::Pop skip every one of the nine kVarTail records as a WRAP FILLER.
// Silently, with no checksum anywhere on this ring. MGPipeCallFlags::kHostSpan (1<<3) lands
// on kRecBorrowSlot the same way and would retire those slots on the GPU timeline instead of
// on apply. So: THE HEADER CARRIES RING FRAMING FLAGS, and the call's own flags are read from
// the opcode through MGPipeCallFlagsFor (R-13.4), which is why that table was generated in the
// first place. The static_asserts below pin the collision so it cannot be re-introduced by
// someone who reads the comment and not this file.
#include <MG_Remote/Transport/LinkMetrics.h>
#include "PipeWireCodec.h"
#include <MG_Remote/FatalFunnel.h>
#include <Config.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
// R-6's tier gate, and the ONE spelling of it (b1's file, unchanged by this package): the
// MapPersistent arm below asks it the same question MGPipeApplyMapPersistent asks.
#include <MG_Remote/Client/PersistentMapTracker.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include <MG_Util/Debug/Log.h>
// P6 gate 8's two wire counters. MG_Util is BELOW MG_Remote in the build order and the pull build
// has no MG_Remote at all, which is why every counter this file touches is behind
// #if MOBILEGL_PIPE_PUSH - the same guard the header's producer readings live under.
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Remote::Wire {
using namespace MobileGL::MG_Pipe;
// 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");
// ---- the collision, asserted EXHAUSTIVELY rather than described -------------------
//
// Naming the two overlaps somebody happened to notice is not enough: the review found a
// THIRD (kReplySlot == kRecVarTail) that four hand-written asserts had missed, and the
// fourth would have been found by a user. So the assertions below cover the whole of both
// enums - every bit, the exact overlap mask, and the completeness of the translation - and
// a new enumerator on either side breaks the build rather than being dropped in silence.
namespace FlagSpace {
constexpr Uint16 kCallAll = static_cast<Uint16>(MG_Pipe::kNeedsAck) |
static_cast<Uint16>(MG_Pipe::kHasBlob) |
static_cast<Uint16>(MG_Pipe::kVarTail) |
static_cast<Uint16>(MG_Pipe::kHostSpan) |
static_cast<Uint16>(MG_Pipe::kReplySlot) |
static_cast<Uint16>(MG_Pipe::kOptional);
constexpr Uint16 kRingAll = static_cast<Uint16>(Transport::kRecNeedsAck) |
static_cast<Uint16>(Transport::kRecHasBlob) |
static_cast<Uint16>(Transport::kRecPad) |
static_cast<Uint16>(Transport::kRecBorrowSlot) |
static_cast<Uint16>(Transport::kRecVarTail) |
static_cast<Uint16>(Transport::kRecNoReply);
// The three call flags the encoder TRANSLATES, and the three it deliberately drops
// because MGPipeCallFlagsFor(op) recovers them from the opcode.
constexpr Uint16 kTranslated = static_cast<Uint16>(MG_Pipe::kNeedsAck) |
static_cast<Uint16>(MG_Pipe::kHasBlob) |
static_cast<Uint16>(MG_Pipe::kVarTail);
constexpr Uint16 kDropped = static_cast<Uint16>(MG_Pipe::kHostSpan) |
static_cast<Uint16>(MG_Pipe::kReplySlot) |
static_cast<Uint16>(MG_Pipe::kOptional);
} // namespace FlagSpace
static_assert(FlagSpace::kCallAll == 0x3Fu,
"MGPipeCallFlags gained or lost an enumerator; re-derive the translation in "
"EncodeRecord and extend the overlap assertions below before assuming the "
"new bit is safe to drop");
static_assert(FlagSpace::kRingAll == 0x3Fu,
"RingRecordFlags gained or lost an enumerator; the two spaces share one "
"16-bit field, so a new ring bit may now alias a call flag");
static_assert((FlagSpace::kTranslated | FlagSpace::kDropped) == FlagSpace::kCallAll &&
(FlagSpace::kTranslated & FlagSpace::kDropped) == 0,
"every MGPipeCallFlags bit must be either translated or deliberately "
"dropped; a seventh enumerator that needs a ring bit would otherwise be "
"dropped in silence");
// ALL SIX CALL-FLAG BITS NOW ALIAS A RING BIT. The sixth, kOptional, was free "by luck
// rather than by design" (RingRecordFlags had not reached 1<<5) until P12 gave bit 5 a
// meaning of its own, kRecNoReply. The alias is harmless for the usual reason - kOptional is
// in kDropped, so it is never stamped into this field, and kRecNoReply is only ever set by
// the encoder's translation - and the named assert below is where that is stated rather than
// assumed. What it costs is the WARNING this comment used to carry: there is no spare bit
// left, so the next call flag or ring flag needs a wider field, not another enumerator.
static_assert((FlagSpace::kCallAll & FlagSpace::kRingAll) == FlagSpace::kRingAll,
"the overlap between the two flag spaces moved");
// Named individually so a failure says WHICH pair, and so the three that actually bite are
// impossible to overlook while reading.
static_assert(static_cast<Uint16>(MG_Pipe::kNeedsAck) ==
static_cast<Uint16>(Transport::kRecNeedsAck),
"kNeedsAck == kRecNeedsAck (harmless: the meanings agree)");
static_assert(static_cast<Uint16>(MG_Pipe::kHasBlob) ==
static_cast<Uint16>(Transport::kRecHasBlob),
"kHasBlob == kRecHasBlob (harmless: the meanings agree)");
static_assert(static_cast<Uint16>(MG_Pipe::kVarTail) ==
static_cast<Uint16>(Transport::kRecPad),
"kVarTail == kRecPad - THE DANGEROUS ONE. Stamping MGPipeCallFlags into the "
"header would make RingConsumer::Pop discard all nine kVarTail opcodes as "
"wrap fillers. The encoder must translate, never stamp; if this ever stops "
"being true, keep translating anyway - two flag spaces in one field is the "
"hazard, not this particular overlap");
static_assert(static_cast<Uint16>(MG_Pipe::kHostSpan) ==
static_cast<Uint16>(Transport::kRecBorrowSlot),
"kHostSpan == kRecBorrowSlot - a stamped host-span record would look like a "
"slot borrowed into the GPU timeline and retire on completedFrameSerial");
static_assert(static_cast<Uint16>(MG_Pipe::kReplySlot) ==
static_cast<Uint16>(Transport::kRecVarTail),
"kReplySlot == kRecVarTail - live in the REVERSE direction: the encoder "
"stamps kRecVarTail on nine opcodes, and a reader who believes "
"PipeWire.inc's 'MGPipeCallFlags of the call' comment reads those nine as "
"kReplySlot");
static_assert(static_cast<Uint16>(MG_Pipe::kOptional) ==
static_cast<Uint16>(Transport::kRecNoReply),
"kOptional == kRecNoReply - the alias P12 created. Harmless because kOptional "
"is in kDropped (never stamped) and kRecNoReply is only ever set by the "
"encoder: a reader that saw a stamped kOptional would read \"do not answer\" off "
"a flag that means \"this call may be absent from the backend table\". If this "
"pair ever stops being true, keep translating anyway - the hazard is two flag "
"spaces in one field, not this particular overlap");
// ---- and the two headers really are one layout -----------------------------------
//
// The decoder walks backwards from RingRecordView::payload to the MGPWireRecHeader in
// front of it, so the two structs being separately asserted to be eight bytes is not
// enough: if RingRecordHeader ever reorders its fields, every flag assert above still
// passes and every payload read shifts by the difference.
static_assert(sizeof(MGPWireRecHeader) == sizeof(Transport::RingRecordHeader));
static_assert(offsetof(MGPWireRecHeader, Op) == offsetof(Transport::RingRecordHeader, kind),
"MGPWireRecHeader::Op and RingRecordHeader::kind are the same two bytes");
static_assert(offsetof(MGPWireRecHeader, Flags) ==
offsetof(Transport::RingRecordHeader, flags),
"MGPWireRecHeader::Flags and RingRecordHeader::flags are the same two bytes");
static_assert(offsetof(MGPWireRecHeader, Size) == offsetof(Transport::RingRecordHeader, size),
"MGPWireRecHeader::Size and RingRecordHeader::size are the same four bytes");
// ---------------------------------------------------------------------------------
// The catalogue, once. Name and payload type per opcode.
// ---------------------------------------------------------------------------------
//
// ONE LIST, THREE READERS: the name table (Fatal lines), the payload-size table (the tail
// arithmetic's base) and the decoder's own completeness assertion. Hand-maintained lists
// that say the same thing three times are how a catalogue edit lands in two of them.
// gen_pipe.py owns PipeCalls.def and PipeWire.inc; this list is held to them by the
// kOpCount assertion under it, which is the strongest statement a non-generated file can
// make about a generated enum.
#define MGPW_FOR_EACH_CALL(X) \
X(GetCaps, MGPCaps) \
X(ResourceCreate, MGPResourceDesc) \
X(ResourceRespecify, MGPResourceDesc) \
X(ResourceDestroy, MGPHandleOnly) \
X(MapPersistent, MGPHandleOnly) \
X(UnmapPersistent, MGPHandleOnly) \
X(FenceCreate, MGPHandleOnly) \
X(FenceStatus, MGPHandleOnly) \
X(FenceWait, MGPFenceWait) \
X(FenceDestroy, MGPHandleOnly) \
X(QueryCreate, MGPQueryDesc) \
X(QueryBegin, MGPQueryDesc) \
X(QueryEnd, MGPQueryDesc) \
X(QueryAvailable, MGPHandleOnly) \
X(QueryResult, MGPQueryResultRequest) \
X(QueryDestroy, MGPHandleOnly) \
X(CreateRenderState, MGPRenderStateDesc) \
X(BindRenderState, MGPBindRenderState) \
X(DeleteRenderState, MGPHandleOnly) \
X(CreateVertexElements, MGPVertexElements) \
X(BindVertexElements, MGPHandleOnly) \
X(DeleteVertexElements, MGPHandleOnly) \
X(CreateSamplerState, MGPSamplerDesc) \
X(DeleteSamplerState, MGPHandleOnly) \
X(CreateSamplerView, MGPSamplerView) \
X(DeleteSamplerView, MGPHandleOnly) \
X(CreateShaderState, MGPProgramDesc) \
X(BindShaderState, MGPHandleOnly) \
X(DeleteShaderState, MGPHandleOnly) \
X(SetDynamicState, MGPDynamicState) \
X(SetFramebufferState, MGPFramebufferState) \
X(SetVertexBuffers, MGPVertexBuffers) \
X(SetIndexBuffer, MGPIndexBuffer) \
X(SetIndirectBuffers, MGPIndirectBuffers) \
X(SetSamplerViews, MGPSamplerViews) \
X(BindSamplerStates, MGPSamplerStates) \
X(SetShaderImages, MGPShaderImages) \
X(SetShaderBuffers, MGPShaderBuffers) \
X(SetStreamOutputTargets, MGPStreamOutputTargets) \
X(SetGlobalConstants, MGPGlobalConstants) \
X(SetVertexAttribDefaults, MGPVertexAttribDefaults) \
X(SetPixelPackState, MGPPixelPackState) \
X(SetPatchState, MGPPatchState) \
X(SetDrawProgram, MGPHandleOnly) \
X(SetDispatchProgram, MGPHandleOnly) \
X(SetResidualValueState, MGPResidualValueState) \
X(SetTextureParams, MGPTextureParams) \
X(ResourceSubData, MGPSubData) \
X(BufferSubDataResident, MGPSubData) \
X(ResourceSubDataComplete, MGPSubDataComplete) \
X(ResourceFlushRange, MGPFlushRange) \
X(ResourceReadback, MGPReadback) \
X(ResourceCopyRegion, MGPCopyRegion) \
X(GenerateMipmap, MGPMipPlan) \
X(GetTextureImage, MGPReadbackInfo) \
X(Blit, MGPBlit) \
X(Clear, MGPClear) \
X(ReadPixels, MGPReadbackInfo) \
X(DrawVbo, MGPDrawInfo) \
X(LaunchGrid, MGPGridInfo) \
X(MemoryBarrier, MGPMemoryBarrier) \
X(BeginStreamOutput, MGPStreamOutputBegin) \
X(EndStreamOutput, MGPXfbAccounting) \
X(PauseStreamOutput, MGPStreamOutputControl) \
X(ResumeStreamOutput, MGPStreamOutputControl) \
X(Flush, MGPFlush) \
X(Present, MGPPresent) \
X(SetSwapInterval, MGPSwapInterval) \
X(QueryTimestamp, MGPTimestampRequest) \
X(QueryCounter, MGPQueryDesc) \
X(FenceWaitServer, MGPFenceWait) \
X(BindShaderImage, MGPImageBind) \
X(PatchParameter, MGPPatchParameter) \
X(BindStreamOutput, MGPStreamOutputBind) \
X(SetStorageBlockBinding, MGPStorageBlockBinding) \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \
X(ApplierReset, MGPApplierReset) \
X(ObjectDeath, MGPHandleOnly) \
X(SetContextValues, MGPContextValues) \
X(SetProgramBindings, MGPProgramBindings) \
X(DeleteStreamOutput, MGPStreamOutputBind) \
X(ReadPixelsToBuffer, MGPReadbackToBuffer) \
X(GetTextureImageToBuffer, MGPReadbackToBuffer) \
X(BindContext, MGPBindContext)
namespace {
constexpr Uint64 Align8(Uint64 value) { return (value + 7u) & ~Uint64(7u); }
#define MGPW_NAME_ROW(Name, Payload) #Name,
constexpr const char* kWireOpNames[] = {
"<kInvalid>",
MGPW_FOR_EACH_CALL(MGPW_NAME_ROW)
};
#undef MGPW_NAME_ROW
// The catalogue grew or shrank. Add the new opcode to MGPW_FOR_EACH_CALL, give it an
// arm in ApplyChecked, and decide in CONTRACT-P5.md table 1 whether it carries bytes.
static_assert(sizeof(kWireOpNames) / sizeof(kWireOpNames[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"PipeCalls.def and this file's call list disagree");
// The payload struct's own sizeof, which is what the tail arithmetic starts from.
// NOT sizeof(MGPWireRec_X) - 8: three payloads are not multiples of 8 and the record
// is rounded up around them.
#define MGPW_SIZE_ROW(Name, Payload) sizeof(MG_Pipe::Payload),
constexpr Uint64 kWirePayloadBytes[] = {
0,
MGPW_FOR_EACH_CALL(MGPW_SIZE_ROW)
};
#undef MGPW_SIZE_ROW
static_assert(sizeof(kWirePayloadBytes) / sizeof(kWirePayloadBytes[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount));
// PH-1 (3): the pre-gate in DecodeAndApply (AdmitsTheGeneratedGate) asks "shorter than
// its own type" with this file's payload list, and the generated MGP_WIRE_CHECK_BOUNDS
// asks it with sizeof(MGPWireRec_X). Pinned equal row by row, so a record the pre-gate
// admits can never be one the generated gate would still abort on.
#define MGPW_REC_ROW(Name, Payload) \
static_assert(sizeof(MG_Pipe::MGPWireRec_##Name) == \
Align8(sizeof(MGPWireRecHeader) + sizeof(MG_Pipe::Payload)), \
"MGPWireRec_" #Name " and this file's payload row disagree on size");
MGPW_FOR_EACH_CALL(MGPW_REC_ROW)
#undef MGPW_REC_ROW
// The header's Size field is 32 bits by wire contract (Ring.h's kMaxRingCapacity
// comment). Every count-derived total is checked against this BEFORE it is used, so a
// corrupt Count can never wrap the arithmetic that is supposed to catch it.
constexpr Uint64 kMaxRecordBytesOnTheWire = 0xFFFFFFFFull;
// MGPResidualValueState's blob is the block itself, which only ever ratchets DOWN.
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE);
// CreateShaderState's seven blob members are one contiguous run, which is what lets
// the honesty pass walk them as an array.
static_assert(offsetof(MGPProgramDesc, Reflection) ==
offsetof(MGPProgramDesc, Spirv) + 6 * sizeof(MGPBlobRef),
"MGPProgramDesc's seven MGPBlobRefs must stay contiguous");
static_assert(offsetof(MGPCaps, RendererInfo) ==
offsetof(MGPCaps, FormatCapabilities) + sizeof(MGPBlobRef),
"MGPCaps's two MGPBlobRefs must stay contiguous");
// Where an op's MGPBlobRef members live inside its payload. Ten rows: the eight
// kHasBlob calls plus the two R-13.1 corrected ones. A row's Count is how many
// CONSECUTIVE MGPBlobRefs start at Offset.
struct BlobSlots {
Uint32 Offset = 0;
Uint32 Count = 0;
};
BlobSlots BlobSlotsFor(MGPWireOp op) {
switch (op) {
case MGPWireOp::GetCaps:
return {static_cast<Uint32>(offsetof(MGPCaps, FormatCapabilities)), 2};
case MGPWireOp::CreateRenderState:
return {static_cast<Uint32>(offsetof(MGPRenderStateDesc, Blob)), 1};
case MGPWireOp::CreateVertexElements:
return {static_cast<Uint32>(offsetof(MGPVertexElements, Blob)), 1};
case MGPWireOp::CreateSamplerState:
return {static_cast<Uint32>(offsetof(MGPSamplerDesc, Parameters)), 1};
case MGPWireOp::CreateShaderState:
return {static_cast<Uint32>(offsetof(MGPProgramDesc, Spirv)), 7};
case MGPWireOp::SetDynamicState:
return {static_cast<Uint32>(offsetof(MGPDynamicState, Blob)), 1};
case MGPWireOp::SetGlobalConstants:
return {static_cast<Uint32>(offsetof(MGPGlobalConstants, Blob)), 1};
case MGPWireOp::SetResidualValueState:
return {static_cast<Uint32>(offsetof(MGPResidualValueState, Blob)), 1};
case MGPWireOp::ResourceSubData:
case MGPWireOp::BufferSubDataResident:
return {static_cast<Uint32>(offsetof(MGPSubData, Blob)), 1};
// P5b: the block NAME rides SEG_STAGE (CONTRACT-P5B.md i1).
case MGPWireOp::SetStorageBlockBinding:
return {static_cast<Uint32>(offsetof(MGPStorageBlockBinding, Name)), 1};
default:
return {};
}
}
Uint32 PopCount32(Uint32 value) {
Uint32 count = 0;
while (value != 0) {
value &= value - 1u;
++count;
}
return count;
}
// The one place a count-times-element-size becomes a byte count. Uint32 * a small
// constant cannot overflow 64 bits, and the total is bounded above before anyone
// indexes with it.
Uint64 TailBytesFor(Uint64 count, Uint64 elementBytes) { return count * elementBytes; }
// The process resolver is a plain function pointer with no user datum
// (MGPipeHostSpan.h:46), so the table it resolves through has to be found here.
//
// P14 S2: TWO PLACES, IN THIS ORDER. The CALLING THREAD's binding is the authoritative one
// - a served session's threads are bound to that session's table (ThreadSessionScope), so
// two sessions in one process never resolve each other's segments. The process-wide table
// below is the fallback for a thread with no binding, which is every shape that has only
// one session (and is what the thread-bound call reduces to there).
SegmentTable* g_processResolverTable = nullptr;
thread_local SegmentTable* t_threadResolverTable = nullptr;
const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 size) {
if (SegmentTable* const bound = t_threadResolverTable) {
return bound->Resolve(seg, offset, size);
}
if (g_processResolverTable == nullptr) {
return nullptr;
}
return g_processResolverTable->Resolve(seg, offset, size);
}
// The decoder currently inside MGPipeApplyWireRecord. Thread-local rather than a file
// static: there is one apply thread by construction, but a unit suite drives several
// decoders from several test threads and a shared slot would make those cases depend
// on each other.
thread_local PipeWireDecoder* t_activeDecoder = nullptr;
} // namespace
const char* WireOpName(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
if (index >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return "<opcode>";
}
return kWireOpNames[index];
}
// ---------------------------------------------------------------------------------
// The Fatal arms, worded once
// ---------------------------------------------------------------------------------
void WireProtocolFatal(const char* what, const char* detail) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : "");
}
void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what,
static_cast<unsigned long long>(got), static_cast<unsigned long long>(expected));
}
// PH-1 (3): the same two lines through the latch (PipeWireCodec.h says where each is used).
Bool WireProtocolLatch(const char* what, const char* detail) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what,
detail != nullptr ? detail : "");
}
Bool WireProtocolLatchAt(const char* what, Uint64 got, Uint64 expected) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu",
what, static_cast<unsigned long long>(got),
static_cast<unsigned long long>(expected));
}
// ---------------------------------------------------------------------------------
// SegmentTable
// ---------------------------------------------------------------------------------
void SegmentTable::AttachLink(Transport::ILink* link) {
m_link = link;
for (Uint32 id = kSegCmd; id <= kSegEvent; ++id) {
const auto segment = static_cast<Transport::LinkSegment>(id);
const Uint64 size = link ? link->SegmentSize(segment) : 0;
const void* base = nullptr;
if (link && link->ResolveSpan(segment, {0, size}, &base) != MOBILEGL_OK) base = nullptr;
Install(static_cast<SegmentId>(id), {const_cast<void*>(base), size});
}
}
void SegmentTable::Install(SegmentId seg, SegmentView view) {
if (seg == kSegNone || static_cast<Uint32>(seg) > static_cast<Uint32>(kSegAdopt)) {
WireProtocolFatalAt("SegmentTable::Install", static_cast<Uint64>(seg),
static_cast<Uint64>(kSegAdopt));
}
m_views[static_cast<SizeT>(seg)] = view;
}
SegmentView SegmentTable::Get(SegmentId seg) const {
if (seg == kSegNone || static_cast<Uint32>(seg) > static_cast<Uint32>(kSegAdopt)) {
return SegmentView{};
}
return m_views[static_cast<SizeT>(seg)];
}
const void* SegmentTable::Resolve(Uint32 seg, Uint64 offset, Uint64 size) const {
// 0 is "no segment", ALWAYS (table 0), and 0xFFFFFFFF is the RESERVED index-mirror
// sentinel (P8-F: the mirror was never built, so nothing resolves it and no client of
// ours sends it). Both land here as "unknown", and the CALLER
// escalates to Fatal - a unit case has to be able to exercise this arithmetic without
// dying.
if (seg == kSegNone || seg > static_cast<Uint32>(kSegAdopt)) {
return nullptr;
}
if (size == 0) {
return nullptr;
}
if (m_link) {
const void* bytes = nullptr;
return m_link->ResolveSpan(static_cast<Transport::LinkSegment>(seg), {offset, size}, &bytes)
== MOBILEGL_OK ? bytes : nullptr;
}
const SegmentView& view = m_views[static_cast<SizeT>(seg)];
if (view.Base == nullptr) {
return nullptr;
}
// Written as a subtraction so offset + size cannot wrap: both are attacker-controlled
// in the only sense that matters here - they came off a shared page.
if (offset > view.Size || size > view.Size - offset) {
return nullptr;
}
return static_cast<const Uint8*>(view.Base) + offset;
}
void SegmentTable::BindThreadResolver() { t_threadResolverTable = this; }
void SegmentTable::UnbindThreadResolver() { t_threadResolverTable = nullptr; }
void SegmentTable::InstallProcessResolver() {
if (t_threadResolverTable != nullptr && t_threadResolverTable != this) {
// Table 3, P14 S2: one thread resolves through ONE table. Two tables on one thread is
// the shape this line exists to make loud - a session's threads are per session, so a
// well-formed process never reaches it, while a second SESSION (its own threads, its
// own table) is exactly what the per-thread binding is for and does not.
WireProtocolFatal("SegmentTable::InstallProcessResolver",
"a process segment resolver is already installed ON THIS THREAD for "
"another session's table; a served session's threads belong to one "
"session");
}
if (MG_Pipe::gMGPipeSegmentResolver == nullptr) {
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk;
}
g_processResolverTable = this;
BindThreadResolver();
}
void SegmentTable::UninstallProcessResolver() {
// Teardown order (table 3): uninstall AFTER the join, never before - a record still in
// flight can still resolve. P14 S2: this unbinds the calling thread and drops the
// process-wide fallback only; the hook stays installed because another session of this
// process may still be decoding records (ServerSession::Close gates this call on the last
// session).
UnbindThreadResolver();
g_processResolverTable = nullptr;
}
// ---------------------------------------------------------------------------------
// R-2's honesty arms
// ---------------------------------------------------------------------------------
Bool CheckBlobIsHonest(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
if (blob.Size == 0) {
if (blob.Seg != kSegNone || blob.Offset != 0) {
// The monolith shape: Seg = None, Offset = a host address, Size = 0. Legal
// there, and precisely what must not cross (rule A). Reading it as "absent"
// would silently drop the bytes of every record an unconverted emitter sent.
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} a blob with Size 0 "
"declares Seg=%u Offset=%llu; under split a blobref is either fully "
"declared or all three fields zero",
WireOpName(op), static_cast<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset));
}
return true;
}
if (blob.Seg == kSegNone) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} Size=%llu with no segment "
"(R-2.3): a non-zero Size must name a real SEG_*",
WireOpName(op), static_cast<unsigned long long>(blob.Size));
}
// R-2.3's SECOND HALF: "inside SOME segment" IS NOT THE RULE. Contract table 1 gives
// every client->server content blob - groups A, B and C, all nineteen rows - the ONE
// carrier SEG_STAGE, and R-10 sends blobs there whole. Until this arm existed the only
// test was that the run resolved, so `CreateSamplerState.Parameters={Seg=SEG_REPLY,...}`
// was accepted and APPLIED: a server-owned segment, whose reuse is the reply pool's
// business and has nothing to do with stage retirement, carrying bytes the applier
// then read. It also went unpoisoned - NoteResolvedRun skipped every non-stage carrier
// - so rule C's only mechanical control read zero on exactly the record that needed it.
//
// The segment is checked BEFORE the resolve, deliberately: a forged SEG_REPLY run that
// happens to lie inside a mapped reply pool must be refused for naming the wrong
// carrier, not left to pass or fail on whether that pool is mapped at all.
//
// NOT A NEW FATAL FAMILY. The review suggested `Fatal{BlobNotStaged}`; this is
// ProtocolCorruption like every other R-2 honesty arm, because the families are the
// vocabulary the operator and the CI greps share (ProtocolCorruption, AbiMismatch,
// UnmigratedVerb, UnmigratedPipeInput, UnsetCallMask, RingOverrun) and a one-off
// seventh name would be a token nothing else in the tree recognises. The SEGMENT is in
// the message, which is what has to be greppable.
if (blob.Seg != kSegStage) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu "
"size=%llu is not SEG_STAGE(%u); every client->server content blob is "
"staged whole in SEG_STAGE (contract table 1 groups A/B/C, R-10) and no "
"other segment may carry one",
WireOpName(op), static_cast<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset),
static_cast<unsigned long long>(blob.Size),
static_cast<unsigned>(kSegStage));
}
if (segments.Resolve(blob.Seg, blob.Offset, blob.Size) == nullptr) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} seg=%u offset=%llu size=%llu "
"does not lie inside that segment (R-2.3)",
WireOpName(op), static_cast<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Offset),
static_cast<unsigned long long>(blob.Size));
}
return true;
}
Bool RequireDeclaredBlob(MGPWireOp op, const MGPBlobRef& blob, const SegmentTable& segments) {
if (blob.Size == 0) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s.blob\"} the record's own fields say "
"it carries content and its blob declares none (R-2.2 / rule A)",
WireOpName(op));
}
return CheckBlobIsHonest(op, blob, segments);
}
Bool CheckHostSpanIsHonest(const MGHostSpan& span) {
if (span.Ptr != nullptr) {
// Rule B. In one address space this pointer WORKS, which is the whole reason the
// rule has to be mechanical: an inproc implementation that kept using it is
// indistinguishable from a correct one until the day it is a second process.
return WireProtocolLatch("host-span",
"MGHostSpan::Ptr is non-null under split; the encoder writes "
"nullptr and names SEG_STAGE (R-2.1)");
}
if (span.Size != 0 && span.Seg == kMGHostSpanSegNone) {
return WireProtocolLatchAt("host-span.seg", span.Size, 0);
}
if (span.Size == 0 && span.Seg != kMGHostSpanSegNone) {
return WireProtocolLatchAt("host-span.size", span.Seg, 0);
}
return true;
}
Bool CheckHostSpanIsHonest(const MGHostSpan& span, const SegmentTable& segments) {
if (!CheckHostSpanIsHonest(span)) return false;
if (span.Size == 0) {
// Fully absent, and the arms above already proved Seg agrees with that.
return true;
}
// ARM 4, WHICH THE OTHER OVERLOAD CANNOT DO. A span naming a real segment and a run
// past the end of it used to pass every check and reach WireVerbSink::OnDrawVbo, which
// this file's header promises is "a DECODED, VALIDATED argument list". It resolves to
// nullptr through MGPipeHostBytes - a draw from a null index pointer - or, for any
// consumer that adds Offset to its own SEG_STAGE base instead of going through the
// resolver, reads outside the segment. P5 emits no spans, so this was latent; P8 arms
// it, which is exactly when nobody will be reading this code.
if (segments.Resolve(span.Seg, span.Offset, span.Size) == nullptr) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"host-span\"} seg=%u offset=%llu "
"size=%llu does not lie inside that segment (R-2.3 arm 4)",
static_cast<unsigned>(span.Seg),
static_cast<unsigned long long>(span.Offset),
static_cast<unsigned long long>(span.Size));
}
return true;
}
Bool CheckDrawUserIndices(const MGPDrawInfo& info, const MGPDrawRange* ranges,
const MGHostSpan& span) {
if ((info.Flags & kDrawHasUserIndices) == 0 || (info.Flags & kDrawIsIndirect) != 0 ||
info.NumDraws != 1 || ranges == nullptr) {
return WireProtocolLatch("DrawVbo.userIndices.shape",
"a user-index span requires exactly one direct indexed range");
}
if (info.IndexSize != 1 && info.IndexSize != 2 && info.IndexSize != 4) {
return WireProtocolLatchAt("DrawVbo.userIndices.IndexSize", info.IndexSize, 4);
}
// Client arrays are staged from their first index; Start only addresses an EBO.
if (ranges[0].Start != 0) {
return WireProtocolLatchAt("DrawVbo.userIndices.Start", ranges[0].Start, 0);
}
const Uint64 required = static_cast<Uint64>(ranges[0].Count) * info.IndexSize;
if (required > span.Size) {
return WireProtocolLatchAt("DrawVbo.userIndices.extent", required, span.Size);
}
return true;
}
// ---------------------------------------------------------------------------------
// The record layout: the tail arithmetic both sides run
// ---------------------------------------------------------------------------------
Bool MGPipeWireRecordLayout(MGPWireOp op, const void* payload, WireRecordLayout& out) {
out = WireRecordLayout{};
const SizeT index = static_cast<SizeT>(op);
if (index == 0 || index >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return false;
}
out.PayloadBytes = kWirePayloadBytes[index];
const Uint64 afterPayload = Align8(sizeof(MGPWireRecHeader) + out.PayloadBytes);
out.TotalBytes = afterPayload;
if (payload == nullptr) {
return true;
}
// Tail sizes. The COUNTS come off the wire, so every product is bounded before it is
// believed, and the bound is the header's own 32-bit Size field.
Uint64 tail0 = 0;
Uint64 tail1 = 0;
Uint64 tail2 = 0; // P5e: set_program_bindings is the one three-tail row
Uint32 tails = 0;
switch (op) {
case MGPWireOp::SetVertexBuffers: {
const auto& p = *static_cast<const MGPVertexBuffers*>(payload);
if (p.Count > kMGPipeMaxVertexAttribs ||
static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxVertexAttribs) {
return WireProtocolLatchAt("SetVertexBuffers.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxVertexAttribs);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPVertexBuffer));
tails = 1;
break;
}
case MGPWireOp::SetSamplerViews: {
const auto& p = *static_cast<const MGPSamplerViews*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxTextureUnits) {
return WireProtocolLatchAt("SetSamplerViews.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxTextureUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPBoundView));
tails = 1;
break;
}
case MGPWireOp::BindSamplerStates: {
const auto& p = *static_cast<const MGPSamplerStates*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxTextureUnits) {
return WireProtocolLatchAt("BindSamplerStates.Count",
static_cast<Uint64>(p.Start) + p.Count, kMGPipeMaxTextureUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPipeHandle));
tails = 1;
break;
}
case MGPWireOp::SetShaderImages: {
const auto& p = *static_cast<const MGPShaderImages*>(payload);
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxImageUnits) {
return WireProtocolLatchAt("SetShaderImages.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxImageUnits);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPImageView));
tails = 1;
break;
}
case MGPWireOp::SetShaderBuffers: {
// TWO TAILS. HostSpanCount is 0 or Count and NEVER anything else
// (MGPipeTypes.h:820-823), because the two arrays stay index-aligned; a third
// value would let a record describe spans for ranges it does not have.
const auto& p = *static_cast<const MGPShaderBuffers*>(payload);
// P5e (sb, CONTRACT-P5E.md §1): the class and the window, bounded HERE as well as
// in the applier and for the reason SetShaderImages' Count is bounded here - this
// is the arm that turns Count into a byte length, so a forged count has to be
// refused before it is multiplied rather than after.
if (p.Class >= kMGPipeShaderBufferClassCount) {
return WireProtocolLatchAt("SetShaderBuffers.Class", p.Class, kMGPipeShaderBufferClassCount);
}
if (static_cast<Uint64>(p.Start) + p.Count > kMGPipeMaxBufferBindingPoints) {
return WireProtocolLatchAt("SetShaderBuffers.Count", static_cast<Uint64>(p.Start) + p.Count,
kMGPipeMaxBufferBindingPoints);
}
if (p.HostSpanCount != 0 && p.HostSpanCount != p.Count) {
return WireProtocolLatchAt("SetShaderBuffers.HostSpanCount", p.HostSpanCount, p.Count);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPBufferRange));
tail1 = TailBytesFor(p.HostSpanCount, sizeof(MGHostSpan));
tails = p.HostSpanCount != 0 ? 2 : 1;
out.SecondTailIsHostSpans = true;
break;
}
case MGPWireOp::SetStreamOutputTargets: {
// TWO TAILS, one Count. MGPBufferRange[Count] then Uint32[Count].
const auto& p = *static_cast<const MGPStreamOutputTargets*>(payload);
tail0 = TailBytesFor(p.Count, sizeof(MGPBufferRange));
tail1 = TailBytesFor(p.Count, sizeof(Uint32));
tails = 2;
break;
}
case MGPWireOp::SetProgramBindings: {
// THREE TAILS, THREE COUNTS, THREE INDEX SPACES (P5e,
// MG_Remote/CONTRACT-P5E.md §1): Int32[BlockBindingCount] in GL uniform-block index
// order, MGPProgramSamplerUnit[SamplerUnitCount] ascending by Location, and
// MGPProgramStorageOverride[StorageOverrideCount] whose Name is a host span staged
// whole - the override map is name-keyed by design.
//
// EACH COUNT IS REFUSED AGAINST ITS DECLARED BOUND, never truncated: a sampler
// array past 256 locations is a program this record cannot describe, and silently
// describing 256 of them would put uniforms on the wrong units with no marker
// anywhere. The counts come off the wire, so every one is bounded before it is
// multiplied.
const auto& p = *static_cast<const MGPProgramBindings*>(payload);
if (p.BlockBindingCount > kMGPipeMaxProgramBlockBindings) {
return WireProtocolLatchAt("SetProgramBindings.BlockBindingCount", p.BlockBindingCount,
kMGPipeMaxProgramBlockBindings);
}
if (p.SamplerUnitCount > kMGPipeMaxProgramSamplerUnits) {
return WireProtocolLatchAt("SetProgramBindings.SamplerUnitCount", p.SamplerUnitCount,
kMGPipeMaxProgramSamplerUnits);
}
if (p.StorageOverrideCount > kMGPipeMaxProgramStorageOverrides) {
return WireProtocolLatchAt("SetProgramBindings.StorageOverrideCount",
p.StorageOverrideCount, kMGPipeMaxProgramStorageOverrides);
}
tail0 = TailBytesFor(p.BlockBindingCount, sizeof(Int32));
tail1 = TailBytesFor(p.SamplerUnitCount, sizeof(MGPProgramSamplerUnit));
tail2 = TailBytesFor(p.StorageOverrideCount, sizeof(MGPProgramStorageOverride));
// ALWAYS THREE, even when a count is 0. The tails are positional and the third one's
// offset is derived from the first two, so a record that declared "one tail" when
// only the block bindings were present would put the same bytes at a different
// offset than a record that declared three - and the two halves of the wire would
// disagree about where the overrides start. An empty tail is zero bytes at an
// 8-aligned offset, which costs nothing.
tails = 3;
// NOT SecondTailIsHostSpans: the spans are MEMBERS of the third tail's elements,
// not a bare MGHostSpan array, so the encoder's blanket honesty pass would read 40
// bytes of {span, binding, pad} as one span and a quarter of garbage. The arm in
// ApplyChecked walks the elements and checks each Name itself.
break;
}
case MGPWireOp::SetVertexAttribDefaults: {
// TWO DECLARANTS THAT MUST AGREE (contract table 1 row 15): Count and
// popcount(Mask). Nothing checks this today; a disagreement is a wire fault that
// scatters attribute values onto the wrong locations.
const auto& p = *static_cast<const MGPVertexAttribDefaults*>(payload);
if (p.Count != PopCount32(p.Mask)) {
return WireProtocolLatchAt("SetVertexAttribDefaults.Count", p.Count, PopCount32(p.Mask));
}
if (p.Count > kMGPipeMaxVertexAttribs) {
return WireProtocolLatchAt("SetVertexAttribDefaults.Count", p.Count, kMGPipeMaxVertexAttribs);
}
tail0 = TailBytesFor(p.Count, sizeof(MGPAttribValue));
tails = 1;
break;
}
case MGPWireOp::ResourceSubData: {
const auto& p = *static_cast<const MGPSubData*>(payload);
tail0 = TailBytesFor(p.RegionCount, sizeof(MGPSubRegion));
tails = 1;
break;
}
case MGPWireOp::BufferSubDataResident: {
// NO TAIL, because its catalogue row has no kVarTail (PipeCalls.def gives it
// kHasBlob|kOptional) and MGPipeApplyBufferSubDataResident takes no regions. The
// layout used to share ResourceSubData's arm, which meant a record with
// RegionCount = 2 was REQUIRED to carry 80 bytes the arm then dropped on the
// floor - and MGPipeBuildSubDataRecord is the shared builder that fills
// RegionCount for both halves, so that was one routing change away from being
// live. The resident path is the BUFFER half only and a buffer record declares no
// regions, so a non-zero count is a fault rather than a tail.
const auto& p = *static_cast<const MGPSubData*>(payload);
if (p.RegionCount != 0) {
return WireProtocolLatchAt("BufferSubDataResident.RegionCount", p.RegionCount, 0);
}
break;
}
case MGPWireOp::DrawVbo: {
// MGPDrawRange[NumDraws], then a CONDITIONAL MGHostSpan. The span's start is
// realigned to 8 because MGPDrawRange is twelve bytes: see WireRecordLayout's
// header comment.
const auto& p = *static_cast<const MGPDrawInfo*>(payload);
tail0 = TailBytesFor(p.NumDraws, sizeof(MGPDrawRange));
const Bool userIndices = (p.Flags & kDrawHasUserIndices) != 0;
const Bool indirect = (p.Flags & kDrawIsIndirect) != 0;
// P5b (CONTRACT-P5B.md d1): the second tail is the user-index span OR the indirect
// block, never both - an indirect draw takes its indices from the bound element
// buffer by GL rule - and an indirect draw declares no ranges, because the server
// never reads the indirect buffer to learn a count and the client has none to send.
if (userIndices && indirect) {
return WireProtocolLatch("DrawVbo.Flags",
"kDrawHasUserIndices and kDrawIsIndirect are exclusive; an "
"indirect draw's indices come from the bound element buffer");
}
if (indirect && p.NumDraws != 0) {
return WireProtocolLatchAt("DrawVbo.NumDraws", p.NumDraws, 0);
}
if (userIndices) {
tail1 = sizeof(MGHostSpan);
tails = 2;
out.SecondTailIsHostSpans = true;
} else if (indirect) {
tail1 = sizeof(MGPDrawIndirect);
tails = 2;
} else {
tails = 1;
}
break;
}
default:
break;
}
if (tails >= 1) {
out.TailOffset[0] = afterPayload;
out.TailBytes[0] = tail0;
out.TotalBytes = afterPayload + tail0;
}
if (tails >= 2) {
out.TailOffset[1] = Align8(out.TailOffset[0] + tail0);
out.TailBytes[1] = tail1;
out.TotalBytes = out.TailOffset[1] + tail1;
}
// P5e: the third tail, realigned to 8 for the second one's reason - set_program_bindings'
// first tail is Int32[] and its elements are four bytes, so the array behind it would
// otherwise start at a 4-aligned offset and every MGHostSpan in the third would be
// misaligned on a host that cares.
if (tails >= 3) {
out.TailOffset[2] = Align8(out.TailOffset[1] + tail1);
out.TailBytes[2] = tail2;
out.TotalBytes = out.TailOffset[2] + tail2;
}
out.TailCount = tails;
out.TotalBytes = Align8(out.TotalBytes);
if (out.TotalBytes > kMaxRecordBytesOnTheWire) {
return WireProtocolLatchAt("record.Size", out.TotalBytes, kMaxRecordBytesOnTheWire);
}
return true;
}
// ---------------------------------------------------------------------------------
// Encoder
// ---------------------------------------------------------------------------------
PipeWireEncoder::PipeWireEncoder(Transport::ILink* link, SegmentTable* segments)
: m_segments(segments) {
if (!link) return;
m_progress = link->Progress(); m_signals = link->Signals(); m_cmd = &link->CommandsOut();
m_link = link->Capabilities().PublishIsDelivery ? nullptr : link;
}
PipeWireEncoder::PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
Transport::RingProducer* stage, SegmentTable* segments)
: m_cmd(cmd), m_stage(stage), m_segments(segments) {
if (control) {
m_progress = &control->Progress;
m_signals = {&control->cmdHead, &control->submittedSeq, &control->consumerParked,
&control->producerParked, &control->eventRingFull};
}
}
Bool PipeWireEncoder::Valid() const { return m_progress != nullptr && m_cmd != nullptr; }
Bool PipeWireEncoder::CheckCancellation() {
if (Cancelled() || (m_cancellationHook && m_cancellationHook(m_cancellationSelf)) ||
(m_stageRetirementBell && m_stageRetirementBell->Dead())) {
m_cancelled = true;
}
return m_cancelled;
}
Uint8* PipeWireEncoder::StageAllocate(Uint64 size) {
if (CheckCancellation()) return nullptr;
if (m_stageBase == nullptr) {
const SegmentView view = m_segments != nullptr ? m_segments->Get(kSegStage)
: SegmentView{};
if (view.Base == nullptr || view.Size == 0) {
WireProtocolFatal("PipeWireEncoder::StageBytes", "SEG_STAGE has no segment view");
}
// The RingProducer c0's constructor takes is the authority on how many of the
// segment's bytes are really the staging area - the rest is whatever the session
// put in front of it. It is read, never written: SEG_STAGE's cursor triple belongs
// to nobody in P5 (see ReclaimStagedBytes' header).
Uint64 capacity = view.Size;
if (m_stage != nullptr && m_stage->Valid()) {
if (m_stage->Capacity() > view.Size) {
WireProtocolFatalAt("SEG_STAGE.capacity", m_stage->Capacity(), view.Size);
}
capacity = m_stage->Capacity();
}
m_stageBase = static_cast<Uint8*>(view.Base);
m_stageCapacity = capacity;
}
const Uint64 need = Align8(size);
if (need > m_stageCapacity) {
SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob cannot fit a "
"%llu byte staging segment at any occupancy; a blob is staged whole, so a "
"row that cuts its content at MGPipeStageChunkBytes() never reaches this - "
"raise MOBILEGL_IPC_STAGE_MB or report the record type with no cut to the "
"integrator",
static_cast<unsigned long long>(size),
static_cast<unsigned long long>(m_stageCapacity));
}
// P6 GATE 8's FIRST NUMBER (CONTRACT-P6.md §9 item 8): the bytes this producer staged. The
// blob's OWN bytes are recorded, from here - where the request first becomes an allocation
// - because this is the ONE place every writer of SEG_STAGE passes through (StageBytes is
// the only caller, and every buffer walk, texture slab, CSO archive and block name reaches
// them through it), which is what makes the count complete by construction rather than by
// a hand-kept list of callers.
//
// `size`, NOT `need`: the Align8 slack is the allocator's, not content the producer wrote,
// and charging it here would make a workload of 8-byte blobs look like one of 16-byte
// blobs. The allocator's wrap skip is excluded for the same reason.
//
// The sample below the byte add is the same call site for the same reason: the per-blob
// distribution is a property of what the PRODUCER handed over, before the arena rounds it.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageSegmentBytes, size);
MG_Util::PipeStats::RecordStagedBlobBytes(size);
}
bool reclaimed = false;
bool waited = false;
for (;;) {
if (CheckCancellation()) return nullptr;
// THE WRAP SKIP MAY ONLY BE CHARGED AGAINST BYTES THAT ARE STILL IN FLIGHT. When
// there are none the allocator starts over at offset zero, so a blob the segment
// can hold whole is never refused (see RebaseEmptyStage). On retry this runs
// AFTER ReclaimStagedBytes, which is the case the finding describes: 8 MiB
// allocated, then retired, then a 28 MiB request that used to abort.
RebaseEmptyStage();
const Uint64 offset = m_stageHead % m_stageCapacity;
// A run is always contiguous: one that would straddle the end skips the remainder,
// exactly as the ring's wrap pad does, and the skipped bytes are reclaimed with
// everything else behind them.
const Uint64 skip = offset + need > m_stageCapacity ? m_stageCapacity - offset : 0;
if ((m_stageHead + skip + need) - m_stageTail <= m_stageCapacity) {
const Uint64 at = (m_stageHead + skip) % m_stageCapacity;
m_stageHead += skip + need;
Transport::LinkMetricsStageBytes(size);
return m_stageBase + at;
}
if (!reclaimed) {
// Lazy reclamation of already-retired bytes is NOT a producer wait.
ReclaimStagedBytes();
reclaimed = true;
continue;
}
if (m_stageRetirementBell == nullptr || m_stageMarkFront == m_stageMarks.size()) break;
const Uint64 pending = m_stageMarks[m_stageMarkFront].Seq;
const auto ready = [&] {
return m_progress->retiredSeq.load(std::memory_order_acquire) >= pending;
};
if (!ready()) {
// The allocation still cannot progress after reclamation. Count this
// blocked allocation once, not each watermark poll or each reclaimed mark.
if (!waited) { ++m_stageReclaimWaits; waited = true; }
// P5e (ra, CONTRACT-P5E §2.6): drain BEFORE parking and again after waking.
// Before, because the ring may already be full and the server already stopped
// - in which case retiredSeq will never move and there is nothing to wake us;
// after, because the wake may have come from a server that is about to fill it
// again. The hook is null in every standalone codec, which has no session and
// therefore no reverse channel to drain.
if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf);
if (m_link) m_link->Flush();
if (CheckCancellation()) return nullptr;
if (!ready() && !m_stageRetirementBell->Wait(*m_signals.ProducerParked, ready, 0,
m_stageWaitTimeoutMs)) {
if (CheckCancellation()) return nullptr;
SessionFail(MGFatalFamily::RetirementWaitFailed,
"MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} live peer did not retire "
"the pending allocation within %u ms", m_stageWaitTimeoutMs);
}
if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf);
}
ReclaimStagedBytes();
}
SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_STAGE\"} a %llu byte blob does not fit a %llu "
"byte staging segment with %llu bytes still in flight (retiredSeq=%llu); a "
"blob is staged whole and only the rows that cut at the stage chunk budget "
"stay small - raise MOBILEGL_IPC_STAGE_MB or report that row to the integrator",
static_cast<unsigned long long>(size),
static_cast<unsigned long long>(m_stageCapacity),
static_cast<unsigned long long>(m_stageHead - m_stageTail),
static_cast<unsigned long long>(
m_progress != nullptr ? m_progress->retiredSeq.load(std::memory_order_acquire) : 0));
}
MGPBlobRef PipeWireEncoder::StageBytes(const void* bytes, Uint64 size) {
if (CheckCancellation()) return {};
if (!Valid() || m_segments == nullptr) {
WireProtocolFatal("PipeWireEncoder::StageBytes",
"no command producer or segment table installed");
}
if (size == 0) {
// "The record declared no blob" and "the record declared an empty blob" must not
// be spelled the same way on a wire (R-2.2), so this is a programming error rather
// than an empty ref.
WireProtocolFatal("PipeWireEncoder::StageBytes",
"a content blob may not declare zero bytes (R-2.2)");
}
if (bytes == nullptr) {
WireProtocolFatal("PipeWireEncoder::StageBytes", "non-zero size with a null source");
}
Uint8* slot = StageAllocate(size);
if (slot == nullptr || CheckCancellation()) return {};
std::memcpy(slot, bytes, static_cast<SizeT>(size));
const Uint64 offset = static_cast<Uint64>(slot - m_stageBase);
if (m_link) m_link->NoteStage({offset, size});
MGPBlobRef ref{};
ref.Seg = kSegStage;
ref.Offset = offset;
ref.Size = size;
ref.Pad0 = 0;
// The self-check that keeps the two halves of "SEG_STAGE" one thing: the segment view
// the decoder resolves through must cover the bytes this allocator just wrote into. A
// view installed over the CONTROL page, or over the segment plus a header, resolves to
// a plausible pointer that is not these bytes.
if (m_segments->Resolve(ref.Seg, ref.Offset, ref.Size) != slot) {
WireProtocolFatal("PipeWireEncoder::StageBytes",
"the SEG_STAGE segment view does not cover the staging area; the "
"two would resolve to different addresses");
}
return ref;
}
Uint64 PipeWireEncoder::EncodeRecord(MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail, Uint64 varTailBytes) {
WireTail tail{varTail, varTailBytes};
return EncodeRecord(op, payload, payloadBytes, &tail, varTail != nullptr ? 1u : 0u);
}
Uint64 PipeWireEncoder::EncodeRecord(MGPWireOp op, const void* payload, Uint64 payloadBytes,
const WireTail* tails, Uint32 tailCount, Bool noReply) {
if (Cancelled()) return kInvalidSeq;
if (!Valid()) {
WireProtocolFatal("PipeWireEncoder::EncodeRecord", "no SEG_CMD producer installed");
}
if (payload == nullptr) {
WireProtocolFatal("PipeWireEncoder::EncodeRecord", "null payload");
}
WireRecordLayout layout{};
if (!MGPipeWireRecordLayout(op, payload, layout)) {
WireProtocolFatalAt("PipeWireEncoder::EncodeRecord",
static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
if (payloadBytes != layout.PayloadBytes) {
WireProtocolFatalAt("EncodeRecord.payloadBytes", payloadBytes, layout.PayloadBytes);
}
// The caller's tails are held to the layout the PAYLOAD declares, which is the same
// arithmetic the decoder will run. A Count that says 4000 while the tail holds 8 bytes
// dies here, on the producing side, rather than on a peer that can only say "corrupt".
//
// A caller may SUPPLY FEWER TAILS THAN THE LAYOUT HAS, but only while the ones it left
// out are empty - Count == 0 is a legal record for every kVarTail row, and requiring a
// {nullptr, 0} entry for it would be a trap rather than a check. Anything else is a
// disagreement between the counts the payload declares and the bytes the caller holds.
if (tailCount > layout.TailCount) {
WireProtocolFatalAt("EncodeRecord.tailCount", tailCount, layout.TailCount);
}
for (Uint32 i = 0; i < layout.TailCount; ++i) {
const Uint64 supplied = i < tailCount ? tails[i].Size : 0;
if (supplied != layout.TailBytes[i]) {
WireProtocolFatalAt("EncodeRecord.tailBytes", supplied, layout.TailBytes[i]);
}
if (supplied != 0 && tails[i].Bytes == nullptr) {
WireProtocolFatal("EncodeRecord.tail", "non-zero tail length with a null pointer");
}
}
if (op == MGPWireOp::DrawVbo && layout.SecondTailIsHostSpans) {
MGHostSpan span{};
std::memcpy(&span, tails[1].Bytes, sizeof(span));
const auto& info = *static_cast<const MGPDrawInfo*>(payload);
// Encoder side: the latch is armed only in a server session child, so here the
// arm dies on a bad shape and its answer carries nothing to act on (PH-1 (3)).
(void)CheckDrawUserIndices(info, static_cast<const MGPDrawRange*>(tails[0].Bytes), span);
}
const Uint64 total = layout.TotalBytes;
if (total > m_cmd->MaxRecordBytes()) {
// R-10's record bound. The content rows cut their BLOBS at the stage chunk budget
// (MGPipeStageChunkBytes); a record's own bytes have no such budget and die here.
SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"%s\"} a %llu byte record exceeds "
"RingProducer::MaxRecordBytes() == %llu (half of a %llu byte SEG_CMD); the stage chunk "
"budget cuts blobs, not a record's own bytes - report the row to the "
"integrator",
WireOpName(op), static_cast<unsigned long long>(total),
static_cast<unsigned long long>(m_cmd->MaxRecordBytes()),
static_cast<unsigned long long>(m_cmd->Capacity()));
}
// MGPipeCallFlags -> RingRecordFlags. See the file header: these are two flag spaces
// in one 16-bit field and stamping the wrong one loses whole opcodes silently.
const Uint32 callFlags = MGPipeCallFlagsFor(op);
Uint16 ringFlags = Transport::kRecNone;
if ((callFlags & static_cast<Uint32>(kNeedsAck)) != 0) ringFlags |= Transport::kRecNeedsAck;
if ((callFlags & static_cast<Uint32>(kHasBlob)) != 0) ringFlags |= Transport::kRecHasBlob;
if ((callFlags & static_cast<Uint32>(kVarTail)) != 0) ringFlags |= Transport::kRecVarTail;
// P12: THE ONE BIT THAT IS NOT A TRANSLATION OF THE OPCODE, because it is not a property
// of the row - set_texture_params waits for an unconfirmed object and does not for a
// confirmed one, so the same opcode answers both ways and only the client knows which.
if (noReply) ringFlags |= Transport::kRecNoReply;
// BOTH WRAP READINGS ARE TAKEN FROM THE PRODUCER'S OWN CURSOR, not from a flag Reserve
// does not return. The cursor is a monotonic byte count and the ring is indexed
// `cursor & mask`, so `cursor / capacity` is the number of times the byte area has been
// reused - and Reserve advances the cursor by `total` for a contiguous record and by
// `spaceToEnd + total` when it had to lay a kRecPad filler down to the boundary first
// (Ring.cpp). Everything below is exact arithmetic over those two facts, which keeps
// both counters in the encoder - where R-10's maximum already lives - rather than
// adding members to a Transport class the wire package does not own.
//
// AND THEY ARE TWO COUNTERS BECAUSE THEY ARE TWO EVENTS, which one measurement made
// unmissable: a workload whose records repeat at a uniform stride that DIVIDES the
// capacity lands on the boundary exactly, every time, for ever. Driving 1310824 bytes
// of clears and draws through a 1 MiB SEG_CMD produced ZERO pads - the ring went round
// once and a half and never straddled - so "did a pad happen" is NOT the question "did
// this ring wrap", and a lane that asked the first one while meaning the second would
// have gone red for a property of its own arithmetic.
//
// m_cmdWraps the head crossed a multiple of the capacity: the ring went round.
// Guaranteed once more bytes are written than the ring holds, which
// is what makes it something exit gate E3(e) can ASSERT.
// m_cmdWrapPads a kRecPad filler was laid because a record would have straddled
// the boundary. R-9's "a pad does not advance seq, both sides skip
// it and count again" is about THIS one, and it is RECORDED rather
// than asserted, because whether it ever happens is a property of
// the record sizes and not of the ring.
const Uint64 headBeforeReserve = m_cmd->LocalHead();
void* slot = m_cmd->Reserve(static_cast<Uint16>(op), ringFlags,
total - sizeof(MGPWireRecHeader));
if (slot != nullptr) {
const Uint64 headAfterReserve = m_cmd->LocalHead();
if ((headAfterReserve - headBeforeReserve) > total) {
++m_cmdWrapPads;
}
const Uint64 capacity = m_cmd->Capacity();
if (capacity != 0) {
// A record is capped at capacity/2 and its pad at capacity/2 too, so one
// Reserve can cross at most one boundary; the subtraction is still written as
// a difference of quotients rather than as a Bool, because that stays correct
// if the cap ever changes.
m_cmdWraps += (headAfterReserve / capacity) - (headBeforeReserve / capacity);
}
}
if (slot == nullptr) {
// The ring is full, not the record too big - Reserve refuses an oversized record
// above, and we already proved this one is not. The caller publishes, waits for
// the apply side and retries.
return kInvalidSeq;
}
auto* bytes = static_cast<Uint8*>(slot);
std::memcpy(bytes, payload, static_cast<SizeT>(payloadBytes));
// Reserve does not zero the alignment padding it hands back, and these bytes leave the
// process under spawn: a record must be a function of what it declares, not of
// whatever the client's ring last held.
const Uint64 payloadSlack = layout.TailCount > 0
? layout.TailOffset[0] - sizeof(MGPWireRecHeader) - payloadBytes
: total - sizeof(MGPWireRecHeader) - payloadBytes;
if (payloadSlack != 0) {
std::memset(bytes + payloadBytes, 0, static_cast<SizeT>(payloadSlack));
}
Uint64 written = layout.TailCount > 0 ? layout.TailOffset[0] - sizeof(MGPWireRecHeader)
: total - sizeof(MGPWireRecHeader);
for (Uint32 i = 0; i < tailCount; ++i) {
const Uint64 at = layout.TailOffset[i] - sizeof(MGPWireRecHeader);
if (at > written) {
std::memset(bytes + written, 0, static_cast<SizeT>(at - written));
}
if (tails[i].Size != 0) {
std::memcpy(bytes + at, tails[i].Bytes, static_cast<SizeT>(tails[i].Size));
}
written = at + tails[i].Size;
}
const Uint64 recordBody = total - sizeof(MGPWireRecHeader);
if (written < recordBody) {
std::memset(bytes + written, 0, static_cast<SizeT>(recordBody - written));
}
// R-2 arms 1 and 3, on the PRODUCING side, over the bytes actually written. Doing it
// here rather than only in the decoder is what makes `inproc` worth running: the
// encoder is the half that can still be wrong in a way the decoder would never see,
// because under inproc a host pointer resolves.
if (m_segments != nullptr) {
const BlobSlots slots = BlobSlotsFor(op);
for (Uint32 i = 0; i < slots.Count; ++i) {
MGPBlobRef blob{};
std::memcpy(&blob, bytes + slots.Offset + i * sizeof(MGPBlobRef), sizeof(MGPBlobRef));
(void)CheckBlobIsHonest(op, blob, *m_segments); // encoder: unarmed, dies (PH-1 (3))
// THE ENCODER MUST NOT ACCEPT A RECORD THE DECODER FATALS ON. w1's ruling that
// CreateShaderState's modules travel inside the Reflection archive lived only
// in the decoder, so an emitter that declared a per-stage run got a valid seq
// here and a Fatal on a peer - exactly the asymmetry EncodeRecord's own
// comment says it exists to prevent.
if (op == MGPWireOp::CreateShaderState && i < 6 && blob.Size != 0) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} "
"declares %llu bytes at the ENCODER; under split the modules travel "
"inside the Reflection archive and the six per-stage runs stay "
"undeclared",
static_cast<unsigned>(i),
static_cast<unsigned long long>(blob.Size));
}
}
}
if ((callFlags & static_cast<Uint32>(kHostSpan)) != 0 && layout.TailCount == 2 &&
layout.SecondTailIsHostSpans && layout.TailBytes[1] != 0 && m_segments != nullptr) {
const Uint64 at = layout.TailOffset[1] - sizeof(MGPWireRecHeader);
const Uint64 spans = layout.TailBytes[1] / sizeof(MGHostSpan);
for (Uint64 i = 0; i < spans; ++i) {
MGHostSpan span{};
std::memcpy(&span, bytes + at + i * sizeof(MGHostSpan), sizeof(MGHostSpan));
// All four arms, including the one that needs the table: a producer that
// computed an offset wrongly is caught here rather than on a peer.
(void)CheckHostSpanIsHonest(span, *m_segments); // encoder: unarmed, dies (PH-1 (3))
}
}
if (total > m_maxRecordBytes) {
m_maxRecordBytes = total;
// WHICH ROW IT WAS, not just how big. R-10 makes the integrator choose between early
// chunking and a bigger default ring when the maximum climbs, and that choice is
// about a specific record family - a var-tail whose length the GL limits bound, or
// one the emitter has to split itself. A number with no row attached leaves the
// reader to guess which, and the guess in c1-v2.md §10 was SetGlobalConstants while
// the measured answer on the reduced path is a different row entirely.
m_maxRecordOp = op;
}
// P6 GATE 8's SECOND NUMBER (CONTRACT-P6.md §9 item 8), and it is counted HERE rather than
// at any emitter for the reason the CallClass comment gives: after chunking, the emitter
// no longer knows how many records its one call produced. By this point the record is
// COMMITTED - Reserve succeeded, the body is written and the blob slots have been checked
// honest - so this is exactly "records/frame post-chunking", not "emitter calls/frame".
//
// A kRecPad FILLER CANNOT REACH HERE: Reserve returns the filler's wake-up as a refusal
// (kInvalidSeq) and the caller retries, so no pad is ever counted, which is what keeps
// this number comparable with the seq space R-3 defines. A REFUSED emission is not counted
// either - the early return above is what makes one record cost exactly one.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireRecords, 1);
}
// THE SAME COMMIT, SPLIT BY OP (P12). The CallClass above answers "how many records"; this
// answers "which rows", which is the question a wait count cannot: the device run had one
// atlas frame pay 43,232 waits while the upload row paid none, so the next thing to know is
// how many records those waits were bought for and by whom. Counted here for the reason the
// comment above gives - after chunking, no emitter knows how many records its call made.
Transport::LinkMetricsNoteRecord(static_cast<std::uint32_t>(op));
++m_emitSeq;
// The stage mark: where SEG_STAGE stood once everything this record names had been
// staged. ReclaimStagedBytes releases up to the newest mark the server has retired.
m_stageMarks.push_back(StageMark{m_emitSeq, m_stageHead});
return m_emitSeq;
}
// THE EMPTY-STAGE REBASE. Head and tail are monotonic byte counts, so "every staged byte
// has retired" reads head == tail, NOT head == tail == 0, and `head % capacity` is left
// wherever the last run ended. Charging a wrap skip against that offset then costs the
// unused suffix a second time: with head == tail == 64 in a 256 KiB stage, the allocator's
// test became `2*capacity - 64 <= capacity`, which is false at EVERY occupancy, so a blob
// that fits the segment whole was refused with `Fatal{RingOverrun, "SEG_STAGE"}` - whose
// own message then reported `0 bytes still in flight`. ReclaimStagedBytes cannot help,
// because an already-empty tail has nothing left to move.
//
// THE MARK QUEUE COMES WITH IT. A mark holds the ABSOLUTE head cursor it was pushed at and
// ReclaimStagedBytes assigns that value straight to m_stageTail. Every mark not yet
// consumed has StageCursor <= head == tail - the head is monotonic and marks are pushed in
// order - so each of them names a region that is already reclaimed and zero is the
// truthful rebasing of it. Without that, one reclaim after a rebase would put the tail
// AHEAD of the head and StagedBytesInFlight() would underflow to about 2^64.
void PipeWireEncoder::RebaseEmptyStage() {
if (m_stageHead != m_stageTail || m_stageHead == 0) {
return;
}
m_stageHead = 0;
m_stageTail = 0;
for (SizeT i = 0; i < m_stageMarks.size(); ++i) {
m_stageMarks[i].StageCursor = 0;
}
}
void PipeWireEncoder::ReclaimStagedBytes() {
if (m_progress == nullptr) {
return;
}
const Uint64 retired = m_progress->retiredSeq.load(std::memory_order_acquire);
Uint64 upTo = m_stageTail;
while (m_stageMarkFront < m_stageMarks.size() &&
m_stageMarks[m_stageMarkFront].Seq <= retired) {
upTo = m_stageMarks[m_stageMarkFront].StageCursor;
++m_stageMarkFront;
}
// COMPACT ON A THRESHOLD, NOT ONLY ON A FULL DRAIN. The queue used to be cleared only
// when front reached size(), so any pipelining deeper than "fully drained at every
// reclaim" - which is precisely the regime this design exists to survive when R-1's
// barrier retires family by family - left front < size() for ever and grew the vector
// 16 bytes per encoded record for the life of the context. Erasing the consumed prefix
// once it is half the queue is amortised O(1) and bounds the storage at twice the
// records actually in flight.
if (m_stageMarkFront == m_stageMarks.size()) {
m_stageMarks.clear();
m_stageMarkFront = 0;
} else if (m_stageMarkFront != 0 && m_stageMarkFront * 2 >= m_stageMarks.size()) {
m_stageMarks.erase(m_stageMarks.begin(),
m_stageMarks.begin() + static_cast<std::ptrdiff_t>(m_stageMarkFront));
m_stageMarkFront = 0;
}
// SEG_STAGE is CLIENT-OWNED memory (contract table 1: "client stages, server copies")
// and the server only reads it, so the client is both the allocator and the thing that
// frees. What it may not do is free ahead of retiredSeq - the whole content of R-11 on
// this side - and what it may ALSO not do is write RingControl's stage tails, which
// Ring.h makes consumer-owned. So the reclaim watermark is this local counter and the
// shared triple is untouched.
if (upTo > m_stageTail) {
m_stageTail = upTo;
}
}
Uint64 PipeWireEncoder::StagedBytesInFlight() const { return m_stageHead - m_stageTail; }
// THE STORAGE, NOT THE LIVE COUNT. `size() - front` is the number of marks still in
// flight, and it stays at one or two even while the vector behind it grows for ever - so a
// control written against it would have gone green through exactly the leak it was meant
// to catch. What leaks is the container, so that is what this reports.
SizeT PipeWireEncoder::StageMarksHeld() const { return m_stageMarks.size(); }
void PipeWireEncoder::Publish() {
if (m_cmd == nullptr) {
return;
}
// Publish then notify. The order is pinned by RingTest and must not be swapped:
// notify-then-publish loses the wakeup. The doorbell itself belongs to the SESSION
// (s1) - the codec does not own a Doorbell and must not, or a unit case could not
// drive encoder -> ring -> decoder without one.
//
// SEG_STAGE needs no publish: the decoder reads those bytes by OFFSET, never by
// popping a ring, and the release store on SEG_CMD's head below is what orders the
// staged writes before the record that names them.
m_cmd->Publish();
if (m_progress != nullptr) {
// submittedSeq is the one watermark the PRODUCER owns (Ring.h's five-watermark
// block). Nobody waits on it; it answers "how far ahead of the server is the
// client right now".
m_signals.SubmittedSeq->store(m_emitSeq, std::memory_order_release);
}
// The reclaim has a trigger in this package, rather than depending on a c1 barrier
// that does not exist yet: every publish is a chance to notice what the server has
// already retired, and it costs one acquire load.
ReclaimStagedBytes();
}
Uint64 PipeWireEncoder::EmitSeq() const { return m_emitSeq; }
Uint64 PipeWireEncoder::MaxRecordBytesSeen() const { return m_maxRecordBytes; }
const char* PipeWireEncoder::MaxRecordOpName() const {
return m_maxRecordOp == MG_Pipe::MGPWireOp::kOpCount ? "none" : WireOpName(m_maxRecordOp);
}
Uint64 PipeWireEncoder::MaxRecordBytesCap() const {
// Read from the ring, not recomputed from MOBILEGL_IPC_RING_MB: the number the proof
// has to hold against is the capacity this process's producer actually got, and the
// two differ the moment a session clamps or rounds the configured size.
return (m_cmd != nullptr && m_cmd->Valid()) ? m_cmd->MaxRecordBytes() : 0;
}
Uint64 PipeWireEncoder::CmdWraps() const { return m_cmdWraps; }
Uint64 PipeWireEncoder::CmdWrapPads() const { return m_cmdWrapPads; }
Uint64 PipeWireEncoder::StageReclaimWaits() const { return m_stageReclaimWaits; }
Uint64 PipeWireEncoder::CmdBytesWritten() const {
// LocalHead(), not RingControl::head: the producer's own cursor includes records
// reserved but not yet published, and this number is about what the PRODUCER wrote.
return (m_cmd != nullptr && m_cmd->Valid()) ? m_cmd->LocalHead() : 0;
}
// ---------------------------------------------------------------------------------
// Decoder
// ---------------------------------------------------------------------------------
PipeWireDecoder::PipeWireDecoder(Transport::ILink* link, SegmentTable* segments, ReplySink* replies)
: m_valid(link && link->Attached()), m_segments(segments), m_replies(replies) {
m_auditPoison = MG_Config::Ipc.Audit;
InstallApplyHook();
}
PipeWireDecoder::PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
ReplySink* replies)
: m_valid(control != nullptr), m_segments(segments), m_replies(replies) {
m_auditPoison = MG_Config::Ipc.Audit;
// Once, at construction, beside the resolver it is modelled on - not per record from
// the apply thread. The thunk is inert without a decoder on the calling thread, so an
// early install changes nothing for a monolith caller of MGPipeApplyWireRecord.
InstallApplyHook();
}
void PipeWireDecoder::InstallApplyHook() {
MG_Pipe::gMGPipeWireRecordApply = &MGPipeWireRecordApplyThunk;
}
void PipeWireDecoder::UninstallApplyHook() { MG_Pipe::gMGPipeWireRecordApply = nullptr; }
Bool PipeWireDecoder::Valid() const { return m_valid && m_segments != nullptr; }
Uint64 PipeWireDecoder::AppliedSeq() const { return m_applySeq; }
void PipeWireDecoder::SetVerbSink(WireVerbSink* sink) { m_verbs = sink; }
WireVerbSink* PipeWireDecoder::VerbSink() const { return m_verbs; }
void PipeWireDecoder::SetAuditPoison(Bool enabled) { m_auditPoison = enabled; }
Bool PipeWireDecoder::AuditPoison() const { return m_auditPoison; }
Uint64 PipeWireDecoder::PoisonedStageBytes() const { return m_poisonedBytes; }
Bool PipeWireDecoder::LastAcceptanceKnown() const { return m_lastAcceptanceKnown; }
Bool PipeWireDecoder::LastAcceptance() const { return m_lastAcceptance; }
Uint64 PipeWireDecoder::AcceptedRecords() const { return m_accepted; }
Uint64 PipeWireDecoder::DeclinedRecords() const { return m_declined; }
Uint64 PipeWireDecoder::AnsweredRecords() const { return m_answered; }
Uint64 PipeWireDecoder::SkippedAnswers() const { return m_skipped; }
void PipeWireDecoder::PostReply(MGPWireOp op, Uint64 seq, Int32 status, const void* bytes,
Uint64 size) {
// THE ONE GATE ON SEG_REPLY. s1 sizes ReplyPool from MGPipeCallFlagsFor - table 0 says
// that table is what "every package" reads - so writing SEG_REPLY[seq % slots] for a
// record the pool reserved no slot for silently overwrites a waiter's answer. And
// because the slot header stamps the WRITER's seq for self-check, the waiter's check
// then fails for ever: the barrier HANGS rather than returning something wrong, which
// is the harder failure to diagnose of the two.
if ((MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot)) == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the decoder tried to answer into "
"a reply slot for a call the catalogue gives no kReplySlot; s1 sizes "
"ReplyPool from MGPipeCallFlagsFor and reserved none",
WireOpName(op));
}
if (m_replies != nullptr) {
m_replies->PostReply(seq, status, bytes, size);
}
}
Bool MGPipeWireRecordApplyThunk(MGPWireOp op, const void* record, Uint64 size, Uint64 remaining) {
(void)remaining;
if (t_activeDecoder == nullptr) {
return false;
}
return t_activeDecoder->ApplyChecked(op, record, size);
}
Bool PipeWireDecoder::NoteResolvedRun(MGPWireOp op, const MGPBlobRef& blob) {
// UNREACHABLE NOW, AND LOUD RATHER THAN SILENT. This used to `return`, and that made
// the audit's bookkeeping quietly optional: a record naming a non-SEG_STAGE carrier
// was applied AND recorded nothing, so PoisonedStageBytes() stayed zero and rule C's
// only mechanical control was dark on exactly the record it existed to catch. The one
// caller is ResolveOrFatal, which runs RequireDeclaredBlob first, and that now refuses
// both an undeclared blob and a non-SEG_STAGE one by name. If either ever arrives here
// the audit has stopped covering the carrier, which is the same failure as no audit at
// all - the reason the run-count overflow just below is a Fatal too.
if (blob.Size == 0 || blob.Seg != kSegStage) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the audit was asked to record a "
"resolved run with seg=%u size=%llu; only declared SEG_STAGE(%u) runs "
"reach the poison fill (R-2.5)",
WireOpName(op), static_cast<unsigned>(blob.Seg),
static_cast<unsigned long long>(blob.Size),
static_cast<unsigned>(kSegStage));
}
if (m_resolvedCount >= sizeof(m_resolved) / sizeof(m_resolved[0])) {
// LOUD, NOT A SILENT DROP. This array is what the 0xDD fill covers, and a poison
// that quietly stopped covering a run is the same failure as no poison at all -
// rule C's only mechanical control going dark without a line in the log.
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} more than %llu SEG_STAGE runs in "
"one record; the audit fill would stop covering them",
WireOpName(op),
static_cast<unsigned long long>(sizeof(m_resolved) / sizeof(m_resolved[0])));
}
m_resolved[m_resolvedCount++] = blob;
return true;
}
// PH-1 (3): nullptr ONLY when a named fault latched (an armed session child); every caller
// returns false on it. Unarmed, each arm below still dies by name and this never returns null.
const void* PipeWireDecoder::ResolveOrFatal(MGPWireOp op, const MGPBlobRef& blob) {
if (!RequireDeclaredBlob(op, blob, *m_segments)) return nullptr;
const void* bytes = m_segments->Resolve(blob.Seg, blob.Offset, blob.Size);
if (bytes == nullptr) {
// RequireDeclaredBlob already ran the same arithmetic, so reaching here means the
// table moved under us rather than that the record is wrong. Same Fatal either
// way: there is no recovery from a segment that stopped covering its own runs.
(void)WireProtocolLatchAt("segment-resolve", blob.Offset, blob.Size);
return nullptr;
}
if (!NoteResolvedRun(op, blob)) return nullptr;
return bytes;
}
void PipeWireDecoder::PoisonResolvedRuns() {
if (!m_auditPoison) {
m_resolvedCount = 0;
return;
}
for (Uint32 i = 0; i < m_resolvedCount; ++i) {
const MGPBlobRef& blob = m_resolved[i];
const SegmentView view = m_segments->Get(kSegStage);
// The SUBTRACTION form, the same one Resolve uses, so the addition cannot wrap.
// Every run here has already been through Resolve, so this is unreachable today -
// but it is the one place in the file where a wrap would be an arbitrary 0xDD
// memset, and "unreachable" is not a reason to write the weaker test.
if (view.Base == nullptr || blob.Offset > view.Size ||
blob.Size > view.Size - blob.Offset) {
continue;
}
// R-2.5. The record has been applied and its bytes are retired, so an applier that
// kept the pointer reads 0xDD next frame instead of bytes that merely happen to
// still be there. Exactly the runs this record resolved, not a conservative window.
std::memset(static_cast<Uint8*>(view.Base) + blob.Offset, 0xDD,
static_cast<SizeT>(blob.Size));
m_poisonedBytes += blob.Size;
}
m_resolvedCount = 0;
}
namespace {
// PH-1 (3): THE GENERATED GATE'S TWO PEER-REACHABLE DEATHS, ASKED FIRST AND BY NAME, in
// an armed session child only. MGP_WIRE_CHECK_BOUNDS (MG_Pipe/generated/PipeWire.inc)
// aborts on an opcode no row names and on a record shorter than its own type or not
// 8-aligned, through MGPipeWireProtocolFatal - a line with no `Fatal{` marker, in a file
// gen_pipe.py owns (fatal_census.py's FUNNEL_SITES records that debt). Both operands are
// the ring header's `kind` and `size`, which the peer writes, so a session child asks the
// same two questions here first and a failure latches by name; whatever reaches the
// generated gate then passes it by construction (MGPW_REC_ROW pins the sizes equal).
// Unarmed - inproc, the unit cases - this admits everything and the generated gate keeps
// its old death line for line.
Bool AdmitsTheGeneratedGate(MGPWireOp op, Uint64 size) {
if (!SessionLatchArmed()) return true;
const SizeT index = static_cast<SizeT>(op);
if (index == 0 || index >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
WireRecordLayout minimum{};
(void)MGPipeWireRecordLayout(op, nullptr, minimum);
// Its own word, not the layout's "record.Size": the two refusals are different
// questions (shorter than the type vs. past the wire's 32-bit Size), and a log line
// - and PeerLatchTest's first-Fatal check - has to be able to tell them apart.
if (size < minimum.TotalBytes || (size % 8) != 0) {
return WireProtocolLatchAt("record.Minimum", size, minimum.TotalBytes);
}
return true;
}
} // namespace
Bool PipeWireDecoder::AdmitOrDecline(const Transport::RingRecordView& record) {
if ((record.flags & Transport::kRecPad) != 0 ||
record.kind == Transport::kRingPadRecordKind) {
return true; // DecodeAndApply's pad arm names it (R-9)
}
const Uint64 size = record.payloadSize + sizeof(MGPWireRecHeader);
if (AdmitsTheGeneratedGate(static_cast<MGPWireOp>(record.kind), size)) return true;
// Latched by name. The record is declined unread and counted, exactly as DecodeAndApply
// counts one it declines: the tally is +1 per record whatever became of it.
m_resolvedCount = 0;
m_lastAcceptanceKnown = false;
++m_applySeq;
return false;
}
Bool PipeWireDecoder::DecodeAndApply(const Transport::RingRecordView& record) {
if (!Valid()) {
WireProtocolFatal("PipeWireDecoder::DecodeAndApply", "no control page or segment table");
}
if ((record.flags & Transport::kRecPad) != 0 ||
record.kind == Transport::kRingPadRecordKind) {
// R-9: a pad does not advance seq, and both sides skip it BEFORE counting.
// RingConsumer::Pop already does; one that reached here has been counted, and a
// seq that drifted by one silently reads another call's reply slot.
WireProtocolFatal("PipeWireDecoder::DecodeAndApply",
"a kRecPad wrap filler reached the decoder; the caller skips pads "
"before counting (R-9)");
}
const auto* base = static_cast<const Uint8*>(record.payload) - sizeof(MGPWireRecHeader);
const Uint64 size = record.payloadSize + sizeof(MGPWireRecHeader);
const MGPWireOp op = static_cast<MGPWireOp>(record.kind);
m_resolvedCount = 0;
m_lastAcceptanceKnown = false;
Bool applied = false;
if (AdmitsTheGeneratedGate(op, size)) {
PipeWireDecoder* previous = t_activeDecoder;
t_activeDecoder = this;
// The generated gate first, ALWAYS: MGPipeApplyWireRecord owns the per-opcode
// `size >= sizeof(MGPWireRec_X)` check, because that is the half that follows from the
// opcode alone and therefore belongs to the generator. It then calls back into
// ApplyChecked through the hook, which owns the half that needs the payload. The hook
// was installed once, at construction.
applied = MG_Pipe::MGPipeApplyWireRecord(op, base, size, size);
t_activeDecoder = previous;
}
PoisonResolvedRuns();
// THE DECODER'S OWN TALLY, AND NOT THE SHARED WATERMARK. RingControl::appliedSeq has
// exactly one writer - s1's SessionConsumer::ApplyOne, +1 per record, pads never
// counted - and this class does not write RingControl at all. Keeping a private count
// beside it is what makes R-9's batching ban CHECKABLE rather than merely stated: the
// session's watermark and this number must agree after every record, and a test that
// compares them catches a batched publish that a single counter could not.
++m_applySeq;
return applied;
}
// ---------------------------------------------------------------------------------
// The 71 arms
// ---------------------------------------------------------------------------------
//
// EVERY ARM ENDS IN AN EXISTING MGPipeApply* FREE FUNCTION, in the verb sink, or in
// `false`. None of them interprets a field: the packed MGPSubData::Target, the
// MGPImageView::Access encoding and MGPFramebufferState::DrawBuffers' -1 token are all
// read by the applier that already owns them, which is why table 0 could say "a decoder
// that open-codes it is the class-1 defect" and this file could obey.
//
// `false` means "this build deliberately does not implement it" and is the answer for the
// 30-odd rows off P5's reduced path (BRIEF §4's exclusion list: the fence family, the
// query family, compute, XFB, indirect, copy-region, GetTextureImage, GenerateMipmap,
// SetShaderBuffers, SetStreamOutputTargets, ResourceSubDataComplete). They are NOT
// unchecked: every one of them runs the same bounds gate, the same tail cross-check and
// the same blob honesty pass before it declines, so the phase that implements one inherits
// a validated record rather than a validation problem.
Bool PipeWireDecoder::ApplyChecked(MGPWireOp op, const void* record, Uint64 size) {
const auto* base = static_cast<const Uint8*>(record);
const void* payload = base + sizeof(MGPWireRecHeader);
// PH-1 (3), ID-P7-1: FROM HERE ON EVERY NAMED FAULT ON A PEER'S BYTES LATCHES. In an armed
// session child each arm below logs its unchanged `Fatal{ProtocolCorruption, "..."}` line
// and returns false, and so does this function: the record is declined, DrainRing sees
// the latch before the next one, and the session closes by name. Unarmed (inproc, unit
// cases) every arm still dies exactly as it did.
WireRecordLayout layout{};
const SizeT opIndex = static_cast<SizeT>(op);
if (opIndex == 0 || opIndex >= static_cast<SizeT>(MGPWireOp::kOpCount)) {
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
// With the opcode known, false here means a count bound in the layout latched.
if (!MGPipeWireRecordLayout(op, payload, layout)) return false;
// THE TAIL CROSS-CHECK (BRIEF §5 w1). The generated gate proved `size >= sizeof(the
// record type)`; it CANNOT SEE THE TAIL, so this is the first thing that holds a
// record declaring Count = 4000 while carrying 8 bytes to its own arithmetic.
if (size != layout.TotalBytes) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and "
"its own count fields describe %llu bytes (fixed payload %llu + tails "
"%llu/%llu/%llu)",
WireOpName(op), static_cast<unsigned long long>(size),
static_cast<unsigned long long>(layout.TotalBytes),
static_cast<unsigned long long>(layout.PayloadBytes),
static_cast<unsigned long long>(layout.TailBytes[0]),
static_cast<unsigned long long>(layout.TailBytes[1]),
static_cast<unsigned long long>(layout.TailBytes[2]));
}
// The record's own ordinal, which is its reply-slot id (R-3). 1-based, and m_applySeq
// only moves after the applier returns.
const Uint64 seq = m_applySeq + 1;
const auto tailAt = [&](Uint32 i) -> const Uint8* {
return layout.TailBytes[i] != 0 ? base + layout.TailOffset[i] : nullptr;
};
// The four Bool-returning appliers answer ACCEPTANCE, not "applied" (R-5: the client
// may not re-derive it, because an if-constexpr discard, a stale handle and a refused
// record are all invisible from the call site).
//
// IT NOW RIDES THE REPLY SLOT, AND THAT IS THE RULING THIS COMMENT ASKED FOR. The text
// that stood here said the answer could not ride a slot "until the integrator rules on
// which half of the contract moves (table 0 says these four use DECLINED; the
// catalogue gives them no slot)". Both halves have since moved the same way: ID-31
// gave `ResourceCreate` kReplySlot and `kMGPipeCallFlags` now carries the flag on all
// four (PipeWire.inc rows 2, 3, 47, 48), and P5 ruling R-17 states that the acceptance
// answers "come back through the reply slot inside the barrier's wait". So the
// conflict is resolved in favour of table 0, the pool reserves these seqs like any
// other, and `PostReply`'s own Fatal - which trips on a row with no kReplySlot - is
// what keeps this honest if a flag is ever taken away again.
//
// WITHOUT THIS THE CLIENT CANNOT RUN AT ALL: `ClientSession::EmitAndWait` asks the
// catalogue, not the caller, whether a row owns a slot, so all four would wait for an
// answer nobody wrote and take `Fatal{ReplyMissing}` inside the barrier. Recording the
// acceptance locally as well is kept, because `LastAcceptance()` and the two counters
// are what the monolith-side decoder cases are asserted on.
//
// OWNERSHIP: MG_Remote/Wire/* is package w1's and w1 is not in wave 2. This hunk is
// four lines inside one lambda, made under R-17 by c1 because it is the server half of
// the routing R-17 assigns, and it is called out in c1-v2.md so the integrator can
// move it if the call belongs elsewhere.
// P12: MAY THIS RECORD'S ANSWER BE SKIPPED? Read from the RING FRAMING field, not from
// MGPipeCallFlagsFor(op): the client decides per CALL, and the opcode cannot answer for
// it (set_texture_params waits for an unconfirmed object and does not for a confirmed
// one). MGPWireRecHeader and RingRecordHeader are the same eight bytes, so `record` -
// which is the header, with the payload behind it - is where the producer's bit is.
const Bool answerSkipped =
(static_cast<const MGPWireRecHeader*>(record)->Flags &
static_cast<Uint16>(Transport::kRecNoReply)) != 0;
const auto noteAcceptance = [&](Bool accepted) {
m_lastAcceptanceKnown = true;
m_lastAcceptance = accepted;
if (accepted) {
++m_accepted;
} else {
++m_declined;
}
// THE BOOKKEEPING ABOVE IS NOT CONDITIONAL, and the skip is placed BELOW it rather
// than around it on purpose: m_accepted / m_declined / LastAcceptance are the
// acceptance record (MG_Test asserts on them), while the PostReply below is the WIRE
// cost this bit exists to remove. A row whose answer is skipped still has to say what
// it decided - "nobody reads the answer" is not "nothing happened".
if (answerSkipped) {
++m_skipped;
return;
}
++m_answered;
// DECLINED is a real answer and carries no payload (ReplySlot.h): the four Bool
// rows say `false` with it, exactly as MapPersistent says nullptr with it.
PostReply(op, seq, accepted ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
nullptr, 0);
};
switch (op) {
// ---- screen ---------------------------------------------------------------------
case MGPWireOp::GetCaps:
// The SESSION answers this one: the snapshot is built from the server's live
// backend and posted through ServerSession::PublishCapsSnapshot (s1/v1), and the
// two blob serializers it needs are MG_Remote/CapsCodec's. The codec validates the
// record and declines, rather than inventing a caps source of its own.
for (Uint32 i = 0; i < 2; ++i) {
MGPBlobRef blob{};
std::memcpy(&blob,
base + sizeof(MGPWireRecHeader) + offsetof(MGPCaps, FormatCapabilities) +
i * sizeof(MGPBlobRef),
sizeof(MGPBlobRef));
if (!CheckBlobIsHonest(op, blob, *m_segments)) return false;
}
return false;
// ---- resources -------------------------------------------------------------------
case MGPWireOp::ResourceCreate:
noteAcceptance(MGPipeApplyResourceCreate(*static_cast<const MGPResourceDesc*>(payload)));
return true;
case MGPWireOp::ResourceRespecify: {
const auto& desc = *static_cast<const MGPResourceDesc*>(payload);
// R-13.3: `initialBytes` IS ALWAYS NULL UNDER SPLIT. Initial content arrives as
// resource_subdata records immediately after this one, which is what the texture
// path already does (TextureEmit.h:1137).
//
// The scope and exact mutable-level extent are one carrier value. Read each part
// through the helpers; forgetting the presence byte would turn a whole-resource
// respecify into level 0 of upload target 0.
MGPRespecifiedLevel level{};
const MGPRespecifiedLevel* scope = nullptr;
if (!MGPipeRespecifyIsWholeResource(desc)) {
level = MGPipeMakeRespecifiedLevel(MGPipeRespecifiedUploadTargetOf(desc),
MGPipeRespecifiedLevelOf(desc),
MGPipeRespecifiedWidthOf(desc),
MGPipeRespecifiedHeightOf(desc),
MGPipeRespecifiedDepthOf(desc));
scope = &level;
}
noteAcceptance(MGPipeApplyResourceRespecify(desc, nullptr, scope));
return true;
}
case MGPWireOp::ResourceDestroy:
MGPipeApplyResourceDestroy(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::MapPersistent: {
// P11 B2 (CONTRACT-P11.md B2): A T0 SESSION'S ADOPTION RIDES THIS RECORD. The verb
// sink takes the client's AHardwareBuffer for this seq off the aux socket and imports
// it (Server/AdoptInbox.h says why that wait cannot hang the apply thread); it answers
// for itself or says "not T0", and everything below is the T2 arm, unchanged.
Int32 t0Status = ReplySink::kStatusDeclined;
if (m_verbs != nullptr &&
m_verbs->OnMapPersistent(*static_cast<const MGPHandleOnly*>(payload), seq, t0Status)) {
PostReply(op, seq, t0Status, nullptr, 0);
return true;
}
}
// R-6 / R-2.4: A CONSTANT DECLINE IN P5, and the applier is not called at all.
// Two reasons, and the second is the one worth writing down: the answer is a HOST
// POINTER, which cannot cross; and MGPWireRec_MapPersistent's payload is a bare
// MGPHandleOnly, so the record carries NEITHER the `size` NOR the `seedBytes` the
// entry point takes. The phase that lands a real map_persistent needs a payload
// change (MGPipeTypes.h, c0's file) before it can even call the applier.
//
// DECLINED is a real answer, not a failure: the three frontend sites already
// tolerate it (BufferObject.cpp:238, :603-606, :657-660).
//
// THE TIER IS CONSULTED HERE, AND IT IS THE SAME CONJUNCTION THE MONOLITH APPLIER
// USES (PipeApply.cpp's `Transport != Monolith && AdoptTierIsEmulate()`). The arm
// used to decline UNCONDITIONALLY and AdoptTier had no reference anywhere on the
// codec path, so MOBILEGL_IPC_ADOPT_TIER=0 and =1 - which contract §5 promises
// "parse and are Fatal at use, naming P11" - decoded as an ordinary DECLINED and
// the operator got a run that looked like a working T0. Since P11 A1 the tier is
// settled at the handshake (Transport/AdoptTier.h): a stream refused T0/T1 there by
// name and runs T2, shared segments died there, so a settled session answers true
// and only a map_persistent no handshake settled still aborts by name. The return
// value is deliberately not a branch: every surviving tier declines (R-6).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
(void)MG_Remote::Client::AdoptTierIsEmulate();
}
PostReply(op, seq, ReplySink::kStatusDeclined, nullptr, 0);
return true;
case MGPWireOp::UnmapPersistent:
MGPipeApplyUnmapPersistent(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::FenceCreate:
return m_verbs != nullptr && m_verbs->OnFenceCreate(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::FenceDestroy:
return m_verbs != nullptr && m_verbs->OnFenceDestroy(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::FenceWaitServer:
return m_verbs != nullptr && m_verbs->OnFenceWaitServer(*static_cast<const MGPFenceWait*>(payload));
case MGPWireOp::FenceStatus:
case MGPWireOp::FenceWait: {
Uint32 result = 0;
const Bool ok = m_verbs != nullptr &&
(op == MGPWireOp::FenceStatus
? m_verbs->OnFenceStatus(*static_cast<const MGPHandleOnly*>(payload), result)
: m_verbs->OnFenceWait(*static_cast<const MGPFenceWait*>(payload), result));
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
case MGPWireOp::QueryCreate:
return m_verbs && m_verbs->OnQueryCreate(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryBegin:
return m_verbs && m_verbs->OnQueryBegin(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryEnd:
return m_verbs && m_verbs->OnQueryEnd(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryDestroy:
return m_verbs && m_verbs->OnQueryDestroy(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::QueryCounter:
return m_verbs && m_verbs->OnQueryCounter(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryAvailable: {
Uint32 result = 0;
const Bool ok = m_verbs && m_verbs->OnQueryAvailable(*static_cast<const MGPHandleOnly*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
case MGPWireOp::QueryResult: {
QueryResultReply result{};
const Bool ok = m_verbs && m_verbs->OnQueryResult(*static_cast<const MGPQueryResultRequest*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
case MGPWireOp::QueryTimestamp: {
Int64 result = 0;
const Bool ok = m_verbs && m_verbs->OnQueryTimestamp(*static_cast<const MGPTimestampRequest*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
// ---- CSOs ------------------------------------------------------------------------
case MGPWireOp::CreateRenderState: {
const auto& desc = *static_cast<const MGPRenderStateDesc*>(payload);
const void* chunks = nullptr;
if (desc.ChunkMask != 0) {
// Contract table 1 row 1: nothing reads Blob.Size today, so the decoder is
// where the record is held to it. The expected length is not a guess - the
// chunk table computes it from the mask the record itself carries.
const Uint64 expected =
static_cast<Uint64>(MGPipePipelineChunkBlobBytes(desc.ChunkMask));
if (!RequireDeclaredBlob(op, desc.Blob, *m_segments)) return false;
if (desc.Blob.Size != expected) {
return WireProtocolLatchAt("CreateRenderState.Blob", desc.Blob.Size, expected);
}
chunks = ResolveOrFatal(op, desc.Blob);
if (chunks == nullptr) return false;
} else if (!CheckBlobIsHonest(op, desc.Blob, *m_segments)) {
return false;
}
MGPipeApplyCreateRenderState(desc, chunks);
return true;
}
case MGPWireOp::BindRenderState:
MGPipeApplyBindRenderState(*static_cast<const MGPBindRenderState*>(payload));
return true;
case MGPWireOp::DeleteRenderState:
MGPipeApplyDeleteRenderState(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::CreateVertexElements: {
const auto& desc = *static_cast<const MGPVertexElements*>(payload);
// The blob's length cross-check against the two counts is the applier's already
// (PipeApply.cpp:1990-1999) and is the model every other row copies; the decoder
// adds only what the applier cannot see, which is that the bytes exist at all.
const void* blob = ResolveOrFatal(op, desc.Blob);
if (blob == nullptr) return false;
MGPipeApplyCreateVertexElements(desc, blob);
return true;
}
case MGPWireOp::BindVertexElements:
MGPipeApplyBindVertexElements(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::DeleteVertexElements:
MGPipeApplyDeleteVertexElements(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::CreateSamplerState: {
const auto& desc = *static_cast<const MGPSamplerDesc*>(payload);
// R-13.1 gave this call its kHasBlob. The serialization IS the memcpy - a POD -
// and borderColorForm must survive BYTE FOR BYTE, because all three colour
// representations are always numerically populated and it is the only thing that
// says which one the backend must use (MGPipeTypes.h:425-428).
if (!RequireDeclaredBlob(op, desc.Parameters, *m_segments)) return false;
if (desc.Parameters.Size != sizeof(MobileGL::SamplerParameters)) {
return WireProtocolLatchAt("CreateSamplerState.Parameters", desc.Parameters.Size,
sizeof(MobileGL::SamplerParameters));
}
const void* bytes = ResolveOrFatal(op, desc.Parameters);
if (bytes == nullptr) return false;
// Copied into a local rather than reinterpret_cast in place: the staged run is
// 8-aligned by the ring, but SamplerParameters is a frontend type and this file
// may not assume its alignment requirement is one the ring happens to satisfy.
MobileGL::SamplerParameters parameters{};
std::memcpy(&parameters, bytes, sizeof(parameters));
MGPipeApplyCreateSamplerState(desc, &parameters);
return true;
}
case MGPWireOp::DeleteSamplerState:
MGPipeApplyDeleteSamplerState(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::CreateSamplerView:
MGPipeApplyCreateSamplerView(*static_cast<const MGPSamplerView*>(payload));
return true;
case MGPWireOp::DeleteSamplerView:
MGPipeApplyDeleteSamplerView(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::CreateShaderState: {
const auto& desc = *static_cast<const MGPProgramDesc*>(payload);
// ONE ARCHIVE, NOT SEVEN RUNS - w1's ruling, and the cheapest one available.
// EncodeProgramArtifacts already serialises SpirvArtifacts::generatedSpirv, i.e.
// every stage module, so Reflection names the WHOLE archive and Spirv[0..5] stay
// undeclared. Shipping the modules a second time would double the largest record
// in the catalogue for no reader at all: MGPipeApplyCreateShaderState takes
// (desc, link*, spirv*) and reads the modules out of spirv->generatedSpirv -
// PipeApply.cpp touches neither desc.Spirv[] nor desc.Reflection.
//
// A DECLARED Spirv[i] IS FATAL rather than ignored, for resource_flush_range's
// reason (contract §6.3): a second, forgeable way to say the same thing is worse
// than no way at all. Overturn it by measuring that a per-stage run beats one
// archive - and then the archive has to stop carrying generatedSpirv, in the same
// change, or the two disagree.
for (Uint32 i = 0; i < 6; ++i) {
if (!CheckBlobIsHonest(op, desc.Spirv[i], *m_segments)) return false;
if (desc.Spirv[i].Size != 0) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"CreateShaderState.Spirv[%u]\"} "
"declares %llu bytes; under split the modules travel inside the "
"Reflection archive and the six per-stage runs stay undeclared",
static_cast<unsigned>(i),
static_cast<unsigned long long>(desc.Spirv[i].Size));
}
}
const void* archiveBytes = ResolveOrFatal(op, desc.Reflection);
if (archiveBytes == nullptr) return false;
// P5e (pg), gap G-A: DECODED ONTO THE HEAP AND HANDED TO THE RECORD, not onto this
// stack. Before P5e these were two locals that died at the closing brace and the
// applier kept only the descriptor, so every reflection question the program twin
// later asked went back to the frontend's own ProgramObject - which Link() replaces
// in place, and which a run-ahead client is several records past by then. The
// record adopts the archive here and answers from it for the rest of its life.
//
// AND THE FRAME, not the bare codec stream: ProgramArchive carries the stage of
// each module beside the modules, because SpirvArtifacts does not and StageMask
// cannot stand in for it (two shader objects may share a stage). See
// ProgramArtifactsCodec.h.
auto archive = MakeShared<MG_State::GLState::ProgramArchive>();
if (!MG_State::GLState::DecodeProgramArchive(static_cast<const Uint8*>(archiveBytes),
static_cast<SizeT>(desc.Reflection.Size),
*archive)) {
// PH-5's ArchiveVector bound (ProgramArtifactsCodec.cpp) refuses here too: a
// vector count whose immediate allocation exceeds the archive's remaining bytes
// makes DecodeProgramArchive answer false.
return WireProtocolLatch("CreateShaderState.Reflection",
"DecodeProgramArchive refused the archive - a truncated "
"stream, a codec version mismatch, a struct-size mismatch, or "
"a stage list that does not match the modules it frames");
}
MGPipeApplyCreateShaderState(desc, &archive->Link, &archive->Spirv, archive);
return true;
}
case MGPWireOp::BindShaderState:
MGPipeApplyBindShaderState(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::DeleteShaderState:
MGPipeApplyDeleteShaderState(*static_cast<const MGPHandleOnly*>(payload));
return true;
// ---- working state ---------------------------------------------------------------
case MGPWireOp::SetDynamicState: {
const auto& dyn = *static_cast<const MGPDynamicState*>(payload);
const void* chunks = nullptr;
if (dyn.ChunkMask != 0) {
const Uint64 expected =
static_cast<Uint64>(MGPipeDynamicChunkBlobBytes(dyn.ChunkMask));
if (!RequireDeclaredBlob(op, dyn.Blob, *m_segments)) return false;
if (dyn.Blob.Size != expected) {
return WireProtocolLatchAt("SetDynamicState.Blob", dyn.Blob.Size, expected);
}
chunks = ResolveOrFatal(op, dyn.Blob);
if (chunks == nullptr) return false;
} else if (!CheckBlobIsHonest(op, dyn.Blob, *m_segments)) {
return false;
}
MGPipeApplySetDynamicState(dyn, chunks);
return true;
}
case MGPWireOp::SetFramebufferState:
MGPipeApplySetFramebufferState(*static_cast<const MGPFramebufferState*>(payload));
return true;
case MGPWireOp::SetVertexBuffers:
MGPipeApplySetVertexBuffers(*static_cast<const MGPVertexBuffers*>(payload),
reinterpret_cast<const MGPVertexBuffer*>(tailAt(0)));
return true;
case MGPWireOp::SetIndexBuffer:
MGPipeApplySetIndexBuffer(*static_cast<const MGPIndexBuffer*>(payload));
return true;
case MGPWireOp::SetIndirectBuffers:
// Indirect is off the reduced path (BRIEF §4) and has no applier entry point.
return false;
case MGPWireOp::SetSamplerViews:
MGPipeApplySetSamplerViews(*static_cast<const MGPSamplerViews*>(payload),
reinterpret_cast<const MGPBoundView*>(tailAt(0)));
return true;
case MGPWireOp::BindSamplerStates:
MGPipeApplyBindSamplerStates(*static_cast<const MGPSamplerStates*>(payload),
reinterpret_cast<const MGPipeHandle*>(tailAt(0)));
return true;
case MGPWireOp::SetShaderImages:
MGPipeApplySetShaderImages(*static_cast<const MGPShaderImages*>(payload),
reinterpret_cast<const MGPImageView*>(tailAt(0)));
return true;
case MGPWireOp::SetShaderBuffers: {
// P5e (sb, MG_Remote/CONTRACT-P5E.md §5.6): THE APPLIER ENTRY POINT EXISTS NOW and
// this is the row's sink. The paragraph that stood here said "no applier entry
// point exists, and P5 does not invent one: the call is on BRIEF §4's exclusion
// list" - true for the whole of P5 through P5d, and withdrawn by the phase whose
// entire point is that the server stops walking the client's binding-point table.
//
// THE HOST-SPAN PASS STILL RUNS FIRST, and it runs whether or not the record is
// applied: kCapNeedsHostUboBytes is RESERVED (P8-F, never published), so the second
// tail is always absent here, and this pass is what says so out loud if it ever is
// not. A span that was going to be refused must be refused BEFORE the applier has
// stored the window it rides with.
if (layout.TailCount == 2 && layout.TailBytes[1] != 0) {
const auto* spans = reinterpret_cast<const MGHostSpan*>(tailAt(1));
const Uint64 count = layout.TailBytes[1] / sizeof(MGHostSpan);
for (Uint64 i = 0; i < count; ++i) {
MGHostSpan span{};
std::memcpy(&span, reinterpret_cast<const Uint8*>(spans) + i * sizeof(MGHostSpan),
sizeof(MGHostSpan));
// ALL FOUR ARMS, the segment-range one included: this row and DrawVbo are
// the only two host-span carriers in the catalogue, and an out-of-segment
// span used to pass every check here.
if (!CheckHostSpanIsHonest(span, *m_segments)) return false;
}
}
MGPipeApplySetShaderBuffers(*static_cast<const MGPShaderBuffers*>(payload),
reinterpret_cast<const MGPBufferRange*>(tailAt(0)));
return true;
}
case MGPWireOp::SetStreamOutputTargets:
// No applier entry point, off the reduced path, both tails validated. UNLIKE
// set_shader_buffers above this row stays unemitted for the whole of P5e (§5.7):
// XFB is lockstep, its capture points are span-scoped state latched at Begin, and
// the payload carries a Generation that no applier state has a home for.
return false;
case MGPWireOp::SetProgramBindings: {
// P5e, opcode 80 (MG_Remote/CONTRACT-P5E.md §1). c0e wrote the shape check and left
// the arm declining; PACKAGE pg COMPLETES IT. What the record needs before its
// consumer sees it is its shape - the three declared counts against their bounds
// and the three-tail arithmetic, both of which MGPipeWireRecordLayout above has
// already run - plus the honesty of every name span it carries.
//
// THE SPANS ARE WALKED HERE RATHER THAN BY THE ENCODER'S BLANKET PASS because they
// are MEMBERS of the third tail's 40-byte elements, not a bare MGHostSpan array:
// SecondTailIsHostSpans reads a tail AS spans, and doing that to {span, binding,
// pad} would read one span and a quarter of garbage. All four arms of
// CheckHostSpanIsHonest run, the segment-range one included, so an override name
// pointing outside SEG_STAGE is caught on the way in rather than at the rebuild.
const auto& bindings = *static_cast<const MGPProgramBindings*>(payload);
// RESOLVED HERE AND VALID ONLY FOR THIS CALL (rule C): the names live in the staged
// run this record named, and the applier copies them into the record before it
// returns. A Vector of resolved pointers rather than a second pass inside the
// applier, because segment resolution is the decoder's job and the applier has no
// segment table - it is reached by the monolith adapter too, where the names are
// the frontend's own c_str()s and there is nothing to resolve.
Vector<const char*> overrideNames;
const MGPProgramStorageOverride* overrides = nullptr;
if (layout.TailCount == 3 && layout.TailBytes[2] != 0 && m_segments != nullptr) {
overrides = reinterpret_cast<const MGPProgramStorageOverride*>(tailAt(2));
const Uint64 count = layout.TailBytes[2] / sizeof(MGPProgramStorageOverride);
overrideNames.reserve(static_cast<SizeT>(count));
for (Uint64 i = 0; i < count; ++i) {
MGPProgramStorageOverride entry{};
std::memcpy(&entry, reinterpret_cast<const Uint8*>(overrides) +
i * sizeof(MGPProgramStorageOverride),
sizeof(MGPProgramStorageOverride));
if (!CheckHostSpanIsHonest(entry.Name, *m_segments)) return false;
// THE NUL IS THE CLIENT'S AND IS CHECKED, not assumed: the emitter stages
// the name with its terminator (ProgramEmit.h), and a span whose last byte
// is not 0 would make the applier's String(const char*) walk off the end of
// SEG_STAGE. Honest-but-unterminated is the one shape CheckHostSpanIsHonest
// cannot see.
const auto* name = static_cast<const char*>(
m_segments->Resolve(entry.Name.Seg, entry.Name.Offset, entry.Name.Size));
if (entry.Name.Size == 0 || name == nullptr ||
name[entry.Name.Size - 1] != '\0') {
return WireProtocolLatch("SetProgramBindings.StorageOverrides.Name",
"an override name span is empty or is not "
"NUL-terminated; the applier copies it as a C string");
}
overrideNames.push_back(name);
}
}
MGPipeApplySetProgramBindings(
bindings,
layout.TailBytes[0] != 0 ? reinterpret_cast<const Int32*>(tailAt(0)) : nullptr,
layout.TailBytes[1] != 0 ? reinterpret_cast<const MGPProgramSamplerUnit*>(tailAt(1))
: nullptr,
overrides, overrideNames.empty() ? nullptr : overrideNames.data());
return true;
}
case MGPWireOp::SetGlobalConstants: {
const auto& rec = *static_cast<const MGPGlobalConstants*>(payload);
// The length cross-check is the applier's (PipeApply.cpp:2784, against the
// program's GlobalUboSize, which this record does not carry). Under rule A it
// stops being inert for the first time, which is the whole point of arming Size.
const void* bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
MGPipeApplySetGlobalConstants(rec, bytes);
return true;
}
case MGPWireOp::SetVertexAttribDefaults:
// Count == popcount(Mask) was enforced by the layout above: two declarants that
// must agree, and nothing checked it before (contract table 1 row 15).
MGPipeApplySetVertexAttribDefaults(*static_cast<const MGPVertexAttribDefaults*>(payload),
reinterpret_cast<const MGPAttribValue*>(tailAt(0)));
return true;
case MGPWireOp::SetPixelPackState:
MGPipeApplySetPixelPackState(*static_cast<const MGPPixelPackState*>(payload));
return true;
case MGPWireOp::SetPatchState:
MGPipeApplySetPatchState(*static_cast<const MGPPatchState*>(payload));
return true;
case MGPWireOp::SetDrawProgram:
MGPipeApplySetDrawProgram(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::SetDispatchProgram:
MGPipeApplySetDispatchProgram(*static_cast<const MGPHandleOnly*>(payload));
return true;
case MGPWireOp::SetResidualValueState: {
// THE HARDEST ROW IN TABLE 1, and it is hard for a reason that does not show in
// the payload: MGPipeApplySetResidualValueState takes `const ResidualValueBlock&`
// - not a payload, not a const void* - and MGPResidualValueState is NEVER
// INSTANTIATED on the live path (PipeFill.cpp:2184 passes the block straight to
// the applier). So the encoder had to invent both the record fill and the blob
// fill, and this is the first code in the tree that reads either.
//
// The block is the blob, whole, and its size only ever ratchets DOWN
// (MGL_RESIDUAL_BLOCK_SIZE, 1248 -> 8 at P2, 0 at P13). Requiring exact equality
// rather than ">=" is what makes a client built against an older block a loud
// mismatch instead of a silently short read of CapabilityBits.
const auto& rec = *static_cast<const MGPResidualValueState*>(payload);
if (!RequireDeclaredBlob(op, rec.Blob, *m_segments)) return false;
if (rec.Blob.Size != sizeof(ResidualValueBlock)) {
return WireProtocolLatchAt("SetResidualValueState.Blob", rec.Blob.Size,
sizeof(ResidualValueBlock));
}
const void* bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
ResidualValueBlock block{};
std::memcpy(&block, bytes, sizeof(block));
MGPipeApplySetResidualValueState(block);
return true;
}
case MGPWireOp::SetTextureParams:
noteAcceptance(MGPipeApplySetTextureParams(*static_cast<const MGPTextureParams*>(payload)));
return true;
// ---- transfer ---------------------------------------------------------------------
case MGPWireOp::ResourceSubData: {
const auto& rec = *static_cast<const MGPSubData*>(payload);
const Bool hasExtent = rec.LevelWidth || rec.LevelHeight || rec.LevelDepth;
if (hasExtent && (rec.Target == kMGPipeResourceTargetBuffer || !rec.LevelWidth ||
!rec.LevelHeight || !rec.LevelDepth || rec.LevelWidth > 0x7fffffffu ||
rec.LevelHeight > 0x7fffffffu || rec.LevelDepth > 0x7fffffffu ||
rec.UnionBox.X < 0 || rec.UnionBox.Y < 0 || rec.UnionBox.Z < 0 ||
Uint64(rec.UnionBox.X) + rec.UnionBox.W > rec.LevelWidth ||
Uint64(rec.UnionBox.Y) + rec.UnionBox.H > rec.LevelHeight ||
Uint64(rec.UnionBox.Z) + rec.UnionBox.D > rec.LevelDepth))
return WireProtocolLatch("ResourceSubData.LevelExtent", "invalid full image extent or dirty box");
// Rule A arms both halves. The buffer half already declared a real size in
// monolith and is cross-checked at PipeApply.cpp:702; THE TEXTURE HALF DECLARED 0
// (TextureEmit.h:1265-1267, on the grounds that the byte count was "the server's
// to compute") and under split it must declare too - a length the reader computes
// from the record it is checking is not a bounds check.
const Bool namesABuffer = rec.Target == kMGPipeResourceTargetBuffer;
// THE TEXTURE PREDICATE, TIGHTENED. It used to be "all three extents non-zero",
// which read a 4x4x0 box as carrying nothing and let rule A's arm 2 sit out - so a
// record could describe a real destination and declare no bytes. A box is either
// EMPTY (every extent zero, which is how a pull that needs nothing is spelled) or
// WHOLE; a partially-zero extent is neither, and no emitter produces one.
const Bool boxIsEmpty =
rec.UnionBox.W == 0 && rec.UnionBox.H == 0 && rec.UnionBox.D == 0;
const Bool boxIsWhole =
rec.UnionBox.W != 0 && rec.UnionBox.H != 0 && rec.UnionBox.D != 0;
if (!namesABuffer && !boxIsEmpty && !boxIsWhole) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ResourceSubData\"} the union box "
"%ux%ux%u has a zero extent on some axes and not others; a box is "
"either empty or whole",
rec.UnionBox.W, rec.UnionBox.H, rec.UnionBox.D);
}
const Bool carriesContent = namesABuffer ? MGPipeSubDataBufferSize(rec) != 0
: (boxIsWhole || rec.RegionCount != 0);
const void* bytes = nullptr;
if (carriesContent) {
bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
} else if (!CheckBlobIsHonest(op, rec.Blob, *m_segments)) {
return false;
}
noteAcceptance(MGPipeApplyResourceSubData(
rec, bytes, reinterpret_cast<const MGPSubRegion*>(tailAt(0))));
return true;
}
case MGPWireOp::BufferSubDataResident: {
const auto& rec = *static_cast<const MGPSubData*>(payload);
// kOptional is a CAPABILITY question under split, not a null-pointer one: the
// client gates on kCapResidentSubData through the caps mirror before it emits
// (R-8). By the time a record is here, the answer was already yes.
const void* bytes = nullptr;
if (MGPipeSubDataBufferSize(rec) != 0) {
bytes = ResolveOrFatal(op, rec.Blob);
if (bytes == nullptr) return false;
} else if (!CheckBlobIsHonest(op, rec.Blob, *m_segments)) {
return false;
}
MGPipeApplyBufferSubDataResident(rec, bytes);
return true;
}
case MGPWireOp::ResourceSubDataComplete:
// RETIRED (P9 W2). It was to be the forward terminator of a SERVER-initiated texture
// pull (section 7.1), and no such pull exists: a re-mint reads the GPU level and the
// server's own staged store (notes/p9/W2-REMINT.md). The row stays because opcodes
// only ever grow; no client emits it and a record that claims it is declined.
return false;
case MGPWireOp::ResourceFlushRange:
// R-13.2 / contract §6.3: IT CARRIES NO BYTES AT ALL under split. The ladder it
// drives rewrites its range from the AUTHORITATIVE SHADOW, which rule C makes
// server-owned, so resource_subdata is already the only way bytes reach it and a
// second way would be a forgeable one. AccessFlags cross verbatim, never
// normalised (PipeApply.h:902-903).
MGPipeApplyResourceFlushRange(*static_cast<const MGPFlushRange*>(payload), nullptr);
return true;
case MGPWireOp::ResourceReadback:
// The BYTES go back in SEG_EVENT through OnBufferWriteback (contract table 1 row
// 22) - the destination is the client's shadow and the size is the resource's, not
// a fixed slot's - so the reply slot carries COMPLETION only.
MGPipeApplyResourceReadback(*static_cast<const MGPReadback*>(payload));
PostReply(op, seq, ReplySink::kStatusOk, nullptr, 0);
return true;
// P5b (CONTRACT-P5B.md): the two transfer verbs with no applier reach the sink like the
// class-B five. resource_copy_region = glCopyImageSubData (i1), generate_mipmap =
// glGenerateMipmap (f1). The payloads are plain PODs with no blob and no tail, so the
// bounds gate above is the whole validation; what the sink does with a renderbuffer
// endpoint or an emulation site is the contract's, not the codec's.
case MGPWireOp::ResourceCopyRegion:
return m_verbs != nullptr &&
m_verbs->OnResourceCopyRegion(*static_cast<const MGPCopyRegion*>(payload));
case MGPWireOp::GenerateMipmap:
return m_verbs != nullptr &&
m_verbs->OnGenerateMipmap(*static_cast<const MGPMipPlan*>(payload));
case MGPWireOp::GetTextureImage:
return m_verbs != nullptr &&
m_verbs->OnGetTextureImage(*static_cast<const MGPReadbackInfo*>(payload), seq, m_replies);
// ---- the five class-B verbs: no MGPipeApply* exists, so v1's sink or nothing -------
case MGPWireOp::Blit:
return m_verbs != nullptr && m_verbs->OnBlit(*static_cast<const MGPBlit*>(payload));
case MGPWireOp::Clear:
return m_verbs != nullptr && m_verbs->OnClear(*static_cast<const MGPClear*>(payload));
case MGPWireOp::ReadPixels:
// P5 BLOCKS on read_pixels (ROADMAP.md:21) and the pixels come back in the reply
// slot, which is why ReplyPool::SlotBytes() is sized from the scenario's largest
// read rather than guessed. MGPReadbackInfo has DstOffset/DstSize and no Seg on
// purpose: in P5 the destination is ALWAYS SEG_REPLY (contract table 1 row 23).
return m_verbs != nullptr &&
m_verbs->OnReadPixels(*static_cast<const MGPReadbackInfo*>(payload), seq, m_replies);
case MGPWireOp::DrawVbo: {
const auto& info = *static_cast<const MGPDrawInfo*>(payload);
// The conditional second tail. P5 must produce NONE of these - the reduced path
// draws from a VBO precisely so kDrawHasUserIndices never fires (table 0's cap-bit
// row) - so a span arriving here is a FINDING, not merely a corruption check, and
// CheckHostSpanIsHonest is what makes it one.
const MGHostSpan* userIndices = nullptr;
MGHostSpan span{};
if ((info.Flags & kDrawHasUserIndices) != 0) {
if (layout.TailCount != 2 || layout.TailBytes[1] != sizeof(MGHostSpan)) {
return WireProtocolLatchAt("DrawVbo.userIndices", layout.TailBytes[1],
sizeof(MGHostSpan));
}
std::memcpy(&span, tailAt(1), sizeof(span));
// ALL FOUR ARMS. WireVerbSink's header promises OnDrawVbo "a DECODED,
// VALIDATED argument list"; without the segment-range arm a span whose run
// left SEG_STAGE reached the sink and that promise was false. P5b's d1 is what
// arms this path (client-side index arrays staged whole, CONTRACT-P5B.md d1).
if (!CheckHostSpanIsHonest(span, *m_segments)) return false;
if (!CheckDrawUserIndices(info, reinterpret_cast<const MGPDrawRange*>(tailAt(0)), span))
return false;
userIndices = &span;
}
// P5b d1: the indirect block, in the span's place. The layout already refused a
// record that sets both flags or that declares ranges alongside it.
const MGPDrawIndirect* indirect = nullptr;
MGPDrawIndirect indirectBlock{};
if ((info.Flags & kDrawIsIndirect) != 0) {
if (layout.TailCount != 2 || layout.TailBytes[1] != sizeof(MGPDrawIndirect)) {
return WireProtocolLatchAt("DrawVbo.indirect", layout.TailBytes[1],
sizeof(MGPDrawIndirect));
}
std::memcpy(&indirectBlock, tailAt(1), sizeof(indirectBlock));
indirect = &indirectBlock;
}
return m_verbs != nullptr &&
m_verbs->OnDrawVbo(info, reinterpret_cast<const MGPDrawRange*>(tailAt(0)),
userIndices, indirect);
}
case MGPWireOp::Present:
return m_verbs != nullptr && m_verbs->OnPresent(*static_cast<const MGPPresent*>(payload));
// ---- compute, barriers, XFB: P5b's i1 and t2 rows, to the sink -----------------------
//
// Six existing rows that returned false through P5 ("off the reduced path"). None has
// an MGPipeApply* and none gains one (CONTRACT-P5B.md): the payloads are plain PODs the
// bounds gate has already proved, so each arm hands over and stops.
case MGPWireOp::LaunchGrid:
return m_verbs != nullptr &&
m_verbs->OnLaunchGrid(*static_cast<const MGPGridInfo*>(payload));
case MGPWireOp::MemoryBarrier:
return m_verbs != nullptr &&
m_verbs->OnMemoryBarrier(*static_cast<const MGPMemoryBarrier*>(payload));
case MGPWireOp::BeginStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnBeginStreamOutput(*static_cast<const MGPStreamOutputBegin*>(payload));
case MGPWireOp::EndStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnEndStreamOutput(*static_cast<const MGPXfbAccounting*>(payload));
case MGPWireOp::PauseStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnPauseStreamOutput(*static_cast<const MGPStreamOutputControl*>(payload));
case MGPWireOp::ResumeStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnResumeStreamOutput(*static_cast<const MGPStreamOutputControl*>(payload));
// ---- the five P5b-appended verbs, opcodes 72..76 ---------------------------------------
case MGPWireOp::BindShaderImage:
return m_verbs != nullptr &&
m_verbs->OnBindShaderImage(*static_cast<const MGPImageBind*>(payload));
case MGPWireOp::PatchParameter:
return m_verbs != nullptr &&
m_verbs->OnPatchParameter(*static_cast<const MGPPatchParameter*>(payload));
case MGPWireOp::BindStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnBindStreamOutput(*static_cast<const MGPStreamOutputBind*>(payload));
case MGPWireOp::DeleteStreamOutput:
return m_verbs && m_verbs->OnDeleteStreamOutput(*static_cast<const MGPStreamOutputBind*>(payload));
// P9 (CONTRACT-P9.md §1): the pack-buffer readbacks. No reply sink on purpose - the
// destination is a resource handle and nothing is answered - so a sink that wanted
// one could only be reading the wrong row.
case MGPWireOp::ReadPixelsToBuffer:
return m_verbs != nullptr &&
m_verbs->OnReadPixelsToBuffer(*static_cast<const MGPReadbackToBuffer*>(payload));
case MGPWireOp::GetTextureImageToBuffer:
return m_verbs != nullptr &&
m_verbs->OnGetTextureImageToBuffer(*static_cast<const MGPReadbackToBuffer*>(payload));
case MGPWireOp::SetStorageBlockBinding: {
// The block name is the ONE string on the wire (CONTRACT-P5B.md i1): a kHasBlob
// record whose blob is the NUL-terminated name, Size = strlen + 1, staged whole in
// SEG_STAGE like every other client -> server blob. Copied into a bounded local
// and re-terminated, so a record whose staged bytes forgot the NUL cannot make the
// backend read past the run, and so the pointer the sink sees dies with this call
// (rule C).
const auto& rec = *static_cast<const MGPStorageBlockBinding*>(payload);
constexpr Uint64 kMaxBlockNameBytes = 4096;
if (!RequireDeclaredBlob(op, rec.Name, *m_segments)) return false;
if (rec.Name.Size > kMaxBlockNameBytes) {
return WireProtocolLatchAt("SetStorageBlockBinding.Name", rec.Name.Size, kMaxBlockNameBytes);
}
const void* bytes = ResolveOrFatal(op, rec.Name);
if (bytes == nullptr) return false;
char name[kMaxBlockNameBytes + 1];
std::memcpy(name, bytes, static_cast<SizeT>(rec.Name.Size));
name[rec.Name.Size] = '\0';
if (name[rec.Name.Size - 1] != '\0') {
return WireProtocolLatch("SetStorageBlockBinding.Name",
"the staged block name is not NUL-terminated; Size is strlen + 1");
}
return m_verbs != nullptr && m_verbs->OnSetStorageBlockBinding(rec, name);
}
case MGPWireOp::CopyFramebufferToTexture:
return m_verbs != nullptr &&
m_verbs->OnCopyFramebufferToTexture(
*static_cast<const MGPCopyFromFramebuffer*>(payload));
// ---- P5c's two control records, opcodes 77..78 (MG_Remote/CONTRACT-P5C.md §5) --------
//
// Fixed-size PODs the bounds gate has already proved; neither carries a blob, a tail
// or a reply, so each arm hands over and stops. ObjectDeath's null-handle refusal is
// the sink's, not the codec's: the codec proves SHAPES, and "the client emits nothing
// for an object that never crossed" (§5.2) is a contract fact about the peer.
case MGPWireOp::ApplierReset:
return m_verbs != nullptr &&
m_verbs->OnApplierReset(*static_cast<const MGPApplierReset*>(payload));
case MGPWireOp::ObjectDeath:
return m_verbs != nullptr &&
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// The third control record, and the applier_reset shape exactly: a fixed-size POD with
// no blob, no tail and no reply, so the generated bounds gate is the whole validation
// and the arm hands over. What the sink does with the token - "this session's current
// context" - is a session fact, not a shape.
case MGPWireOp::BindContext:
return m_verbs != nullptr &&
m_verbs->OnBindContext(*static_cast<const MGPBindContext*>(payload));
// ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 -------------
//
// An ordinary set_* row, unlike the two control records beside it: a fixed-width POD
// with no blob, no tail and no reply, so the bounds gate is the whole validation and
// the arm is the applier entry point - the same shape as set_pixel_pack_state beside
// it. There is no sink half and no acceptance answer: the write into gPipeInputs is
// the whole effect.
case MGPWireOp::SetContextValues:
MGPipeApplySetContextValues(*static_cast<const MGPContextValues*>(payload));
return true;
case MGPWireOp::SetSwapInterval:
// Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21).
return false;
case MGPWireOp::Flush:
// NOT A VERB AND NOT A NO-OP DECISION: there is no Flush slot in
// GLFunctionsTable at all, and glFlush/glFinish are empty function bodies
// (Definitions.cpp:111-112), so R-4's predicted minimum set naming Flush was
// wrong (C-3). A TriangleScenario that orders its readback with glFlush orders
// nothing; the verb barrier is what orders it.
return false;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
return WireProtocolLatchAt("opcode", static_cast<Uint64>(op),
static_cast<Uint64>(MGPWireOp::kOpCount));
}
}
} // namespace MobileGL::MG_Remote::Wire