Files
MobileGL/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
T
BZLZHH 4e48702f26 [Feat] (MG_Remote): a record can tell the server its answer will never be read - the bit works and cuts a frame's replies by 92%, and it proves the window's real blocker is the FIFO drain, not the pool depth
The create window could not collect its deferred answers. The previous commit
diagnosed that as the reply pool being 8 slots deep against 55,903 replies in one
frame, and left a no-reply bit as the way out. This lands that bit, measures it on
the device, and reports that the diagnosis was half wrong.

WHAT LANDED. ClientSession::EmitAndWaitTails takes a second question beside
wantReply: willReadReply. They are not the same question and the window is the case
that separates them - it emits with wantReply=false and READS the answer later, so
its records must still be answered. kRecNoReply (RingRecordFlags bit 5, which
aliases kOptional in the call-flag space; the flag-space asserts were extended and
the alias named) is stamped when willReadReply is false, and the decoder skips
PostReply while keeping every acceptance counter - "nobody reads the answer" is not
"nothing happened". The two illegal combinations are Fatal by name.

Three call sites say it: SetTextureParams for a confirmed object, the respecify
row's fire-and-forget half, and sub-data where MGPipeSubDataWantsItsReply decides.

MEASURED ON THE DEVICE (aikjs4s8cys8ba7l, rebuilt APK + client, both halves new):
    server:  skipped=50,001   answered=4,521        (a frame's replies: 55,903 -> 4,543)
    client:  Reply frames received seen=4,501, latest seq=97,181
    frame-sent: identical per op, records untouched
    create row: by_op 2 = 9,070 (was 4,536)  - because the drain's blocking take is
               now counted, so the row's number stays honest instead of reading as
               "create stopped waiting" while the waiting merely moved
AND THE WINDOW STILL READ BACK NOTHING: readfail 8,951 -> 8,872, i.e. -0.9%.

THE REAL CAUSE, from the miss probe:
    P12miss n=1     want=310 replySeq=1504   ring=[1175..1504]
    P12miss n=9001  want=310 replySeq=102915 ring=[102866..102915]
want=310 every time. The drain is STRICTLY FIFO and a failed read has no way out -
it retries the same head for ever. Any reply buffer is bounded by design (8 pool
slots, or the 8-frame ring the stream link now keeps), so with 4,536 deferred
answers at least one is overwritten before it is read, and that one wedges the
window permanently. Cutting the churn by two orders of magnitude does not help: it
turns a certainty into a certainty. That is the constraint, not the depth.

THE FIX IS NAMED AND NOT TAKEN (the goal this work ran under stops here on purpose).
On a failed read of the head, abandon it: MGPipeNoteHandleUnpublished +
NoteCreateSuspect + pop - the DECLINE path reused for "unknown". The window then
self-heals through the blocking path and the object merely gets slower. It needs its
own gate: a failed read must let the window ADVANCE.

Gates, all green (172 + 13 + 22 + 103):
  PipeWireCodecTest.TheNoReplyBitSkipsTheAnswerAndKeepsTheAcceptanceRecord - the skip,
    the kept tally, the answered/skipped split, and the negative control that a record
    WITHOUT the bit is still answered (a server that stopped answering everything
    would otherwise pass)
  RemoteClientControls.ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - now also
    asserts the answers CAME BACK (answers_taken=11, outstanding=1, fallbacks=0 against
    12 records). RED-ONCE PROVED: giving the window's creates the literal
    "fire-and-forget => skip" predicate makes this case fail.
  RingTest - the sixth and last flag alias, and the hand-written exhaustiveness list
    extended (with a note that it is only as exhaustive as that list).
  Server side: AnsweredRecords()/SkippedAnswers() - the number that made this
    debuggable, since a wire change whose effect is invisible is undebuggable.

Client-first is safe: an old server ignores bit 5 and replies as before, so the two
cross-version directions degrade to today's behaviour - this is "both ends must
upgrade to take EFFECT", not "to avoid breaking". Hit that in practice first.
2026-09-26 02:00:44 +08:00

2540 lines
149 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)
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.
SegmentTable* g_processResolverTable = nullptr;
const void* ProcessResolverThunk(Uint32 seg, Uint64 offset, Uint64 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 P8's index-mirror sentinel
// which this phase does not resolve. 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::InstallProcessResolver() {
if (MG_Pipe::gMGPipeSegmentResolver != nullptr && g_processResolverTable != this) {
// Table 3: there is exactly ONE gMGPipeSegmentResolver per process, the SERVER
// role installs it before the apply thread starts, and the client never resolves a
// span at all. Two roles racing on one inline variable is loud rather than silent
// because of this line.
WireProtocolFatal("SegmentTable::InstallProcessResolver",
"a process segment resolver is already installed; the server role "
"installs it once, before the apply thread starts");
}
g_processResolverTable = this;
MG_Pipe::gMGPipeSegmentResolver = &ProcessResolverThunk;
}
void SegmentTable::UninstallProcessResolver() {
// Teardown order (table 3): uninstall AFTER the join, never before - a record still in
// flight can still resolve.
MG_Pipe::gMGPipeSegmentResolver = nullptr;
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:
// 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. AdoptTierIsEmulate returns
// true at T2 and ABORTS at T0/T1 on its own named diagnostic, so the return value
// is deliberately not a branch: P5 declines at every tier it survives (R-6), and
// the two forbidden ones never get this far.
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 0 for the whole of P5 (table 0), 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:
// The forward terminator of a SERVER-initiated texture pull (section 7.1). There
// is no client producer and no applier; the reverse channel is P7/P9.
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));
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));
// ---- 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