Files
MobileGL/MobileGL/MG_Remote/Server/PipeApplier.cpp
T
BZLZHH 8245221755 [Fix] (Remote): name a shared image by extent and format when the kernel gives descriptors one identity
(st_dev, st_ino) is how an incoming dma-buf descriptor names the image it was
exported from, and it is the only identity the registry had. Some kernels do not
keep those apart: on a 4.14 vendor kernel every descriptor of every image reports
dev 12 ino 10658 with st_size 0, and /proc/self/fdinfo carries nothing else, so
the registry correctly detected the ambiguity and then refused every import -
which is every zero-copy window a compositor ever tries to sample, leaving the
whole stack on the wl_shm fallback.

The identity stays the kernel's to give. When it is ambiguous, the import's own
extent and format select among the live images exported under that identity, in
allocation order and each claimed once, which is the order a producer allocates
and hands them over in. Identify() takes the extent and the fourcc the import
record already carries; nothing else changes.
2026-10-03 15:54:55 +08:00

1852 lines
110 KiB
C++

// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs.
#include "PipeApplier.h"
#include <MG_Remote/FatalFunnel.h>
#include "ServerSession.h"
#include "ServerLoop.h"
#include "../Transport/ReplySlot.h"
#include "StagedTextureStore.h"
#include <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
// P5c ct: object_death's per-kind release names the Espryt twin tables (CONTRACT-P5C.md
// §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built
// on Magma simply holds no twins in these tables and every release resolves to nothing.
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <cstdlib>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#include <cstring>
#include <limits>
namespace MobileGL::MG_Remote::Server {
// P5e (ra, CONTRACT-P5E §1 / §6): DOES THIS SERVER PUBLISH kCapRunAheadApply? The
// question is asked of the server's OWN CallMask and not of a build constant, because
// "the client may run ahead" is exactly what that bit says and Magma never sets it.
// A session with no CallMask yet (the bring-up window, a fixture that never called
// SetCapabilityBits) answers false: no client can have latched run-ahead against a
// snapshot that was never published.
//
// P5e (gl, ID-111): PROMOTED OUT OF THE ANONYMOUS NAMESPACE, unchanged in body. It is now
// the second conjunct of the barriered stamp as well as Present's frame-serial gate, and
// the red-once has to be able to assert what it answers for the session it built.
Bool MGPipeServerPublishesRunAhead() {
const ServerSession* session = ServerSession::Active();
if (session == nullptr || !session->CallMaskIsSet()) return false;
return (session->CallMask() & static_cast<Uint64>(MG_Pipe::kCapRunAheadApply)) != 0;
}
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
// PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY
// slot pool is s1's deliverable (BRIEF 5) and its addressing lives in one place,
// Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating
// `seq % slots` on this side is how the two halves come to disagree about which slot an
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
// call's answer instead of failing.
//
// The view is rebuilt per call rather than stored, so that this body does not change
// ReplyPool's four members and therefore does not touch v1's header at all.
void ReplyPool::PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) {
if (m_link) {
const auto result = m_link->PostReply(seq, status, bytes, size);
if (result == MOBILEGL_OK) {
++m_posted;
return;
}
++m_failed;
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL)
SessionFail(MGFatalFamily::ReplyTooLarge, "MGPipe: Fatal{ReplyTooLarge, stream reply}");
// EVERY OTHER FAILURE IS NAMED AND COUNTED RATHER THAN SWALLOWED (P12). This used to
// `return` on anything that was not BUFFER_TOO_SMALL, so a TRANSPORT_CLOSED reply
// vanished with no line, no counter and no Fatal - and the client's only symptom is a
// deferred answer that never arrives, which is indistinguishable from a buffer that
// overwrote it. Once per call site rather than once per reply: a broken link fails
// every time, and a log per answer would drown the run it is there to explain.
MGLOG_E_ONCE("MGPipe: reply seq %llu was NOT delivered (%d); the client will not read"
" this answer. FailedReplies() counts them",
static_cast<unsigned long long>(seq), static_cast<int>(result));
return;
}
Transport::ReplySlotPool pool(m_base, m_size, m_slots);
// Fatal inside Post when the answer does not fit a slot: P5 does not chunk replies,
// and the client knows an answer's size before it emits the record.
pool.Post(seq, status, bytes, size);
++m_posted;
}
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
// -----------------------------------------------------------------------------------
// ServerVerbSink - the five class-B verbs
// -----------------------------------------------------------------------------------
void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) {
if (m_backend != backend) {
ReleaseQueries();
ReleaseFences();
}
m_backend = backend;
if (backend != nullptr) {
const auto& limits = backend->GetDynamicParameters();
ServerStagedTexture().SetDeviceLimits(
limits.MaxTextureSize, limits.Max3DTextureSize, limits.MaxCubeMapTextureSize,
limits.MaxArrayTextureLayers, limits.MaxTextureBufferSize);
}
}
const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const {
if (m_backend == nullptr) {
// DECLINE BY NAME, DO NOT DEREFERENCE. A verb that arrives before
// ServerLoop::CreateBackend has run means the hook order changed under us, and the
// honest answer is "this build did not apply it" - which DecodeAndApply reports as
// false and the lane sees as a record that did not render, rather than as a crash
// with no line saying which verb was first.
MGLOG_E_ONCE("MG_Remote server: %s arrived with no backend object; the verb is "
"DECLINED. ServerLoop::CreateBackend runs from MG_Backend::Init()'s "
"hook, before ClientSession::Start",
verb);
return nullptr;
}
return &m_backend->GetBackendFunctions();
}
ServerVerbSink::FenceEntry& ServerVerbSink::FindFence(MG_Pipe::MGPipeHandle handle) {
const auto it = m_fences.find(handle.Slot);
if (handle.Slot == 0 || it == m_fences.end() ||
!it->second.Live || it->second.Gen != handle.Gen) {
Wire::WireProtocolFatal("Fence.handle", "missing, destroyed or stale fence handle");
}
return it->second;
}
Bool ServerVerbSink::OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) {
ServerSession* session = ServerSession::Active();
return session != nullptr && session->AdoptStoreT0(handle, seq, status);
}
Bool ServerVerbSink::OnFenceCreate(const MG_Pipe::MGPHandleOnly& desc) {
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
const auto* table = Table("FenceCreate");
if (table == nullptr) return false;
const auto handle = desc.Handle;
if (handle.Slot == 0) {
Wire::WireProtocolFatal("FenceCreate.handle", "reserved fence handle");
}
const Bool seen = m_fences.find(handle.Slot) != m_fences.end();
auto& entry = m_fences[handle.Slot];
if (entry.Live || (seen && handle.Gen <= entry.Gen)) {
Wire::WireProtocolFatal("FenceCreate.handle", "duplicate or stale fence generation");
}
entry.Gen = handle.Gen;
entry.Live = true;
// GL_Sync.cpp treats an absent slot or a null creation result as always signaled.
entry.Native = table->GL.FenceSync == nullptr ? nullptr : table->GL.FenceSync();
// P10: in creation order, for ReportSignaledFences - the order the GPU finishes them in.
m_unreportedFences.push_back(handle);
return true;
}
Bool ServerVerbSink::OnFenceDestroy(const MG_Pipe::MGPHandleOnly& desc) {
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
auto& entry = FindFence(desc.Handle);
const auto* table = Table("FenceDestroy");
if (table == nullptr) return false;
if (entry.Native != nullptr && table->GL.DeleteSync != nullptr) table->GL.DeleteSync(entry.Native);
entry.Native = nullptr;
entry.Live = false;
return true;
}
Bool ServerVerbSink::OnFenceStatus(const MG_Pipe::MGPHandleOnly& desc, Uint32& result) {
if (desc.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence))
Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace");
auto& entry = FindFence(desc.Handle);
const auto* table = Table("FenceStatus");
if (table == nullptr) return false;
result = entry.Native == nullptr || table->GL.GetSyncStatus == nullptr ||
table->GL.GetSyncStatus(entry.Native);
return true;
}
Bool ServerVerbSink::OnFenceWait(const MG_Pipe::MGPFenceWait& request, Uint32& result) {
if ((request.Flags & ~static_cast<Uint32>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
Wire::WireProtocolFatal("FenceWait.Flags", "unknown client-wait flag");
}
auto& entry = FindFence(request.Fence);
const auto* table = Table("FenceWait");
if (table == nullptr) return false;
result = entry.Native == nullptr || table->GL.ClientWaitSync == nullptr
? GL_ALREADY_SIGNALED
: table->GL.ClientWaitSync(entry.Native, request.Flags, request.TimeoutNs);
return true;
}
Bool ServerVerbSink::OnFenceWaitServer(const MG_Pipe::MGPFenceWait& request) {
if (request.Flags != 0 || request.TimeoutNs != GL_TIMEOUT_IGNORED) {
Wire::WireProtocolFatal("FenceWaitServer.arguments", "invalid server wait arguments");
}
auto& entry = FindFence(request.Fence);
const auto* table = Table("FenceWaitServer");
if (table == nullptr) return false;
if (entry.Native != nullptr && table->GL.WaitSync != nullptr)
table->GL.WaitSync(entry.Native, request.Flags, request.TimeoutNs);
return true;
}
void ServerVerbSink::ReleaseFences() {
// Detach runs on the apply thread before its private backend/context is destroyed.
if (m_backend != nullptr) {
const auto destroy = m_backend->GetBackendFunctions().GL.DeleteSync;
if (destroy != nullptr) {
for (auto& [slot, entry] : m_fences)
if (entry.Live && entry.Native != nullptr) destroy(entry.Native);
}
}
m_fences.clear();
m_unreportedFences.clear();
}
// P10 (CONTRACT-P10.md §1): WHICH OF THE CLIENT'S FENCES HAS THE GPU FINISHED.
//
// The client answers a poll of a fence it knows is signaled without asking - that is the
// whole of package A - and it learns what is signaled only from here. Walked in CREATION
// order and stopped at the first one still pending: one context's GPU work completes in
// the order it was submitted, so a later fence cannot be done while an earlier one is not,
// and the steady-state cost is one status query per drain batch. (Were a backend ever to
// finish them out of order, the later fence would only be reported late - an answer the
// client then gets by its own round trip - never early.)
//
// `flush`: the apply thread is about to go idle. A fence whose commands the driver still
// holds never signals on its own, and an idle server submits nothing more, so the first
// pending fence is asked through ClientWaitSync(GL_SYNC_FLUSH_COMMANDS_BIT, 0) - the one
// backend-neutral way both backends take to submit what they hold - instead of the plain
// status query. Only when idle: mid-stream the next records flush soon enough, and a
// submit per batch would cost Magma its command-buffer batching.
Uint32 ServerVerbSink::ReportSignaledFences(Bool flush) {
if (m_unreportedFences.empty()) return 0;
ServerSession* session = ServerSession::Active();
if (session == nullptr || m_backend == nullptr) return 0;
const MG_Backend::GLFunctionsTable& gl = m_backend->GetBackendFunctions().GL;
Uint32 reported = 0;
while (!m_unreportedFences.empty()) {
const MG_Pipe::MGPipeHandle handle = m_unreportedFences.front();
const auto it = m_fences.find(handle.Slot);
if (it == m_fences.end() || !it->second.Live || it->second.Gen != handle.Gen) {
// Deleted before it was seen signaled: nobody can poll it any more.
m_unreportedFences.pop_front();
continue;
}
const FenceEntry& entry = it->second;
Bool signaled = entry.Native == nullptr;
if (!signaled) {
if (flush && gl.ClientWaitSync != nullptr) {
const GLenum status = gl.ClientWaitSync(entry.Native, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
signaled = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED;
flush = false; // one submit is all an idle server owes
++m_fenceIdleFlushes;
} else {
signaled = gl.GetSyncStatus == nullptr || gl.GetSyncStatus(entry.Native);
}
}
if (!signaled) break;
m_unreportedFences.pop_front();
session->PostFenceSignaled(handle);
++reported;
}
m_fencesReported += reported;
return reported;
}
#include "QueryServer.inc"
Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
// The named framebuffer record already crossed before this verb. Scope
// only the server's resolved draw target; the next verb still observes
// the application's unchanged binding. Both backends sync from this
// handle and never need the frontend FramebufferObject.
auto& applier = MG_Pipe::MGPipeApplier();
struct ScopedClearTarget {
MG_Pipe::MGPipeApplierState& State;
MG_Pipe::MGPipeHandle Saved;
~ScopedClearTarget() { State.BoundFramebuffer[0] = Saved; }
} clearTarget{applier, applier.BoundFramebuffer[0]};
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo)) {
if (!applier.FramebufferRecordFor(clear.Fbo))
Wire::WireProtocolFatal("Clear.Fbo", "missing named framebuffer record");
applier.BoundFramebuffer[0] = clear.Fbo;
}
switch (clear.Kind) {
case kMGPClearKindWhole:
if (gl.Clear == nullptr) return false;
gl.Clear(static_cast<GLbitfield>(clear.BufferMask));
break;
case kMGPClearKindColor:
switch (clear.ValueClass) {
case kMGPClearValueClassFloat:
if (gl.ClearBufferfv == nullptr) return false;
gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex,
reinterpret_cast<const GLfloat*>(clear.ColorValue));
break;
case kMGPClearValueClassInt:
if (gl.ClearBufferiv == nullptr) return false;
gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex,
reinterpret_cast<const GLint*>(clear.ColorValue));
break;
case kMGPClearValueClassUint:
if (gl.ClearBufferuiv == nullptr) return false;
gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex,
reinterpret_cast<const GLuint*>(clear.ColorValue));
break;
default:
// A value class outside the three is a wire fault, not a fallback: all three
// representations of a clear colour are numerically populated by the frontend
// and only this field says which one the backend must use, so guessing renders
// a plausible wrong colour.
Wire::WireProtocolFatalAt("MGPClear::ValueClass", clear.ValueClass, 3);
}
break;
case kMGPClearKindDepth:
if (gl.ClearBufferfv == nullptr) return false;
gl.ClearBufferfv(GL_DEPTH, 0, &clear.DepthValue);
break;
case kMGPClearKindStencil:
if (gl.ClearBufferiv == nullptr) return false;
gl.ClearBufferiv(GL_STENCIL, 0, &clear.StencilValue);
break;
case kMGPClearKindDepthStencil:
if (gl.ClearBufferfi == nullptr) return false;
gl.ClearBufferfi(GL_DEPTH_STENCIL, 0, clear.DepthValue, clear.StencilValue);
break;
default:
Wire::WireProtocolFatalAt("MGPClear::Kind", clear.Kind, kMGPClearKindDepthStencil + 1);
}
++m_clears;
return true;
}
Bool ServerVerbSink::OnBlit(const MG_Pipe::MGPBlit& blit) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit");
if (table == nullptr) return false;
if (table->GL.BlitFramebuffer == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.3): the record's handles cross to the backend as the verb's
// own state. The bound form carries two nulls and nothing changes; the named form's
// pair is what the backend's named-blit arm resolves - the sink no longer relies on
// "the read and draw framebuffers are already bound" (the client's ScopedBlitBindings
// staging is deleted with this), and a backend that does not consume the pair has no
// named arm, which is a loud decline rather than a blit of whatever is bound.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbBlitReadFbo = blit.ReadFbo;
applierState.VerbBlitDrawFbo = blit.DrawFbo;
const Bool named = !MG_Pipe::MGPipeHandleIsNull(blit.ReadFbo) ||
!MG_Pipe::MGPipeHandleIsNull(blit.DrawFbo);
#endif
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
blit.DstY0, blit.DstX1, blit.DstY1,
static_cast<GLbitfield>(blit.Mask),
static_cast<GLenum>(blit.Filter));
#if MOBILEGL_BUILD_DISAGGREGATED
if (named && !applierState.VerbBlitNamedConsumed) {
MGLOG_E_ONCE("MGPipe: a named blit (read {%u, %u}, draw {%u, %u}) reached a backend "
"with no named-blit arm; the verb is DECLINED rather than applied to "
"the bound framebuffers",
blit.ReadFbo.Slot, blit.ReadFbo.Gen, blit.DrawFbo.Slot, blit.DrawFbo.Gen);
applierState.VerbBlitReadFbo = MG_Pipe::kMGPipeNullHandle;
applierState.VerbBlitDrawFbo = MG_Pipe::kMGPipeNullHandle;
return false;
}
#endif
++m_blits;
return true;
}
namespace {
// A record's damage rectangles as a region; 0 rectangles is the whole surface. A count past
// what the record holds is clamped (the rectangles it names are all there are).
MG_Util::Damage::Region DamageOfRecord(Uint32 count, const Int32* rects) {
const Int32 n = static_cast<Int32>(std::min<Uint32>(count, MG_Pipe::kMGPMaxDamageRects));
return MG_Util::Damage::Region::FromEglRects(rects, n);
}
} // namespace
Bool ServerVerbSink::OnPresent(const MG_Pipe::MGPPresent& present) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("present");
if (table == nullptr) return false;
if (table->Present == nullptr) return false;
// Present is the ONLY frame-boundary drain the backend has (DirectGLES.cpp:12424-12470:
// the fence poll, the four ring OnPresent hooks, TrimBufferPool, PipeStats::OnPresent),
// which is why ARCHITECTURE.md:531 wants present <-> eglSwapBuffers to stay 1:1.
//
// m-1: THAT 1:1 IS A CONVENTION c1 UPHOLDS, NOT A STRUCTURAL GUARANTEE, and the earlier
// claim that it was structural is wrong. This Present() is reached ONLY from a present
// RECORD (ServerVerbSink::OnPresent). ServerSwapEGLBuffers does NOT reach it - it calls
// backend->SwapEGLBuffers -> BackendObject::SwapEGLBuffers -> eglSwapBuffers, and never
// Present() - so the two paths do NOT both end here. The frame count staying in step with
// the swap count rests entirely on c1 emitting exactly one present record per swap;
// nothing here compares Presents() to a swap count. If that drifts, the frame fence and
// TrimBufferPool's recycle watermark stop tracking frames - which is the reason the 1:1
// was wanted, recorded here so a future swap-without-present is looked for rather than
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
// The frame's damage rides to the native present for the length of this call.
MG_Backend::SetCurrentPresentDamage(DamageOfRecord(present.DamageCount, present.Damage));
table->Present();
MG_Backend::SetCurrentPresentDamage(MG_Util::Damage::Region::Full());
++m_presents;
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
// present credit, so the client sends 0 and the frame count on this side IS the serial.
//
// P5e (ra, CONTRACT-P5E §1, §2.4): IT IS THE CLIENT'S NOW, 1-BASED AND MINTED BY THE
// PAYER. A credit can only be paced in an id space the waiter advances, and the waiter
// is the client (`WaitForPresentAck(m_presentsSent + 1 - credit)`), so a server-stamped
// serial would be the server acknowledging its own count. The 0 arm survives for a
// peer that has not been re-built - and on a server that PUBLISHES the run-ahead cap it
// is a protocol fault, because such a client is pacing on this answer and a 0 would
// acknowledge a frame nobody asked about.
if (present.FrameSerial == 0 && MGPipeServerPublishesRunAhead()) {
// PH-1 (3): latches in an armed session child - no credit is returned for it, the
// session closes and the peer's credit wait ends on the hang-up.
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead "
"server was handed present serial 0. The client mints this 1-based and "
"waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for "
"serial 0 would release a wait that is asking about frame N");
}
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
// §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no
// production caller since v1 wrote it: ONE CREDIT PER SWAP, returned after Present()
// has returned rather than before it, because what the client is waiting for is the
// swap and not the record's apply. It advances presentAckSerial AND rings the client's
// bell - a client parked in WaitForPresentAck(kWaitForever) needs the pair.
if (ServerSession* session = ServerSession::Active()) {
// P10 (CONTRACT-P10.md §1): the fences finished by now are reported BEFORE the credit
// goes back - a client released by the credit polls or waits on the fence of a frame
// or two ago at once, and a report posted only after this batch would race it. A
// status query, no submit: the swap has just submitted.
if (session->EventRingHasRoom()) ReportSignaledFences(/*flush=*/false);
session->ReturnPresentCredit(m_lastPresentSerial);
}
return true;
}
Bool ServerVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) {
if (!replies) Wire::WireProtocolFatal("GetTextureImage.reply", "missing reply sink");
const SizeT bpp = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
Wire::WireProtocolFatal("GetTextureImage.extent", "invalid tight image extent");
}
const Uint64 tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D ||
info.DstOffset > tight || !info.DstSize || info.DstSize > tight - info.DstOffset)
Wire::WireProtocolFatal("GetTextureImage.range", "invalid reply window");
auto image = info;
image.DstOffset = 0;
image.DstSize = tight;
Vector<Uint8> bytes;
const Bool ok = ReadTextureImageTight(image, bytes);
if (!ok || bytes.size() != tight) {
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
return false;
}
replies->PostReply(seq, Wire::ReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize);
m_readbackBytes += info.DstSize;
++m_readbacks;
return true;
}
Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) {
if (replies == nullptr) {
// The decoder always passes its ReplySink; a null one means the applier was built
// without a reply pool, and answering nothing would leave the client's barrier
// waiting for a slot that never gets stamped - a hang, not a wrong picture.
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - "
"the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) "
"and a client blocked on seq %llu would never be answered",
static_cast<unsigned long long>(seq));
}
const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type)));
// The server's LinkTerms.maxReplyBytes is copied from its attached ILink capabilities.
// Reject malformed shapes and oversized replies before growing m_readbackScratch. The
// client-side guard is not a trust boundary: a peer can write this record directly.
const auto rejectReadback = [&] {
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
return false;
};
if (bytesPerPixel == 0 || info.Box.W == 0 || info.Box.H == 0 ||
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
info.DstSize == 0 || m_maxReplyBytes == 0) {
return rejectReadback();
}
const Uint64 width = info.Box.W;
const Uint64 height = info.Box.H;
const Uint64 bpp = static_cast<Uint64>(bytesPerPixel);
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
if (width > kUint64Max / height) return rejectReadback();
const Uint64 pixels = width * height;
if (pixels > kUint64Max / bpp) return rejectReadback();
const Uint64 tight = pixels * bpp;
// PH-3's bound is the SERVER's answer size: w*h*bpp against its own maxReplyBytes,
// checked before the scratch grows. It is not a rule about DstSize.
if (tight > m_maxReplyBytes ||
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
return rejectReadback();
}
// ID-49: THE REPLY CROSSES TIGHT, WHATEVER DstSize THE CLIENT SENT. The server reads with
// neutral pack state into a w*h*bpp extent that IS the reply payload and the client
// scatters it per its own GL_PACK_* state, so a DstSize that disagrees with the tight
// extent (a client that sized its destination with pack padding) is logged and the tight
// extent this side owns is read and posted - never the client's number, so a wrong
// DstSize cannot make this a short read into uninitialised scratch. F2's first PH-3 draft
// refused the mismatch instead, which broke the ID-49 control
// (ServerLoopEglTest.AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent); the
// bound above is what PH-3 needs and it does not depend on DstSize.
if (tight != info.DstSize) {
MGLOG_E_ONCE("MG_Remote server: read_pixels DstSize %llu != tight w*h*bpp %llu "
"(%ux%u, bpp %llu); reading the tight extent (ID-49)",
static_cast<unsigned long long>(info.DstSize),
static_cast<unsigned long long>(tight), info.Box.W, info.Box.H,
static_cast<unsigned long long>(bpp));
}
if (!ReadBoundFramebufferTight(info, tight)) {
// A well-formed call can still be unsupported by this server; return the
// established decline status after the peer-controlled size has been checked.
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
return false;
}
// m-7: the answer is written into the slot HERE, mid-apply, while the verb stamp is still
// up - and that is safe for exactly one reason, which is the contract's and is stated so it
// is not mistaken for luck: the client reaches a reply slot ONLY through appliedSeq
// (ReplySlot.h's ORDERING clause), never by polling the slot's own stamp, and s1's
// SessionConsumer::ApplyOne publishes appliedSeq only AFTER PipeApplier::ApplyOne has run
// LeaveApplier() and (on the joint tree) dropped the ScopedApplierEntry. So by the time the
// client is allowed to look at this slot, the apply-side gPipeInputs flag is already down.
replies->PostReply(seq, Wire::ReplySink::kStatusOk, m_readbackScratch.data(), tight);
m_readbackBytes += tight;
++m_readbacks;
return true;
}
Bool ServerVerbSink::ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
if (table == nullptr || table->GL.ReadPixels == nullptr) return false;
if (tight > m_readbackScratch.size()) {
m_readbackScratch.resize(static_cast<SizeT>(tight));
}
// Save the server-visible pack state, force neutral for the read, restore. Both go through
// the applier's own set_pixel_pack_state entry point (MGPipeApplySetPixelPackState writes
// gPipeInputs.m_pixelStore[0], which the backend's ReadPixels reads via
// MGB_CTX->GetPixelStoreParameters); the read is synchronous on this thread, so the window
// in which the pack state is neutral does not outlive the call.
const MG_Pipe::PixelStoreParameters savedPack =
MG_Pipe::gPipeInputs.GetPixelStoreParameters(/*isUnpack=*/false);
// MG_Pipe owns the constant (MGPipeTypes.h): in a verify build the compare-at-read hook's
// oracle for the pack half inside this window is the same value, and a second hand-typed
// copy would drift without a build break.
const MG_Pipe::MGPPixelPackState neutralPack = MG_Pipe::MGPipeNeutralReadPixelsPack();
MG_Pipe::MGPipeApplySetPixelPackState(neutralPack);
table->GL.ReadPixels(info.Box.X, info.Box.Y, static_cast<GLsizei>(info.Box.W),
static_cast<GLsizei>(info.Box.H), static_cast<GLenum>(info.Format),
static_cast<GLenum>(info.Type), m_readbackScratch.data());
MG_Pipe::MGPPixelPackState restorePack{};
restorePack.Pack = savedPack;
MG_Pipe::MGPipeApplySetPixelPackState(restorePack);
// A backend that found its device lost latched this session and returned without filling
// the scratch: the read is declined rather than answered with stale bytes.
return !SessionLatched();
}
Bool ServerVerbSink::ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector<Uint8>& bytes) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_backend && m_backend->GetBackendType() == BackendType::DirectGLES)
return MG_Backend::DirectGLES::ReadTextureImageWire(image, bytes);
if (m_backend && m_backend->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::DirectVulkan::pVulkanRenderer)
return MG_Backend::DirectVulkan::pVulkanRenderer->ReadTextureImageWire(image, bytes);
#endif
(void)image;
(void)bytes;
return false;
}
// ---- P9 (W1, MG_Remote/CONTRACT-P9.md §1): the pack-buffer readbacks ------------------------
//
// WHY THE ROWS GO THROUGH THE APPLIER'S BUFFER-WRITE GATE AND NOT THROUGH THE DRIVER'S OWN
// PBO READ. Monolith binds the driver buffer as GL_PIXEL_PACK_BUFFER and lets glReadPixels
// write it, then maps the whole store back into the frontend shadow (DirectGLES.cpp's
// "PBO used, mapping buffer to client memory"). A server has two stores per buffer - the
// backend's (GL buffer / VkBuffer) and R-11's staged shadow that every later drain uploads
// from - and a driver-side write reaches only the first: the next queued range drained from
// the shadow would then land OLD bytes over the pixels on Espryt, and Magma has no pack path
// in its ReadPixels at all. MGPipeApplyResourceSubData is the one write both backends already
// take from the server (Ops_H_SubData adopts into the staged shadow and queues the range;
// WriteWireBuffer orders the copy against in-flight reads), so the rows are exactly what a
// client glBufferSubData of the same bytes would have been - same serial bump, same epoch
// bump, same ordering against the records that follow.
//
// THE READ IS THE REPLY FORM'S, byte for byte (ReadBoundFramebufferTight /
// ReadTextureImageTight), so the two forms cannot disagree about what a pixel is; only the
// destination moved. The PACK layout is the client's arithmetic (ReadbackLayout) carried in
// the record, and the byte swap is applied to the tight bytes first, exactly where the client
// applied it to its reply.
//
// A FAILED READ IS LOUD. The reply form's failures reach the client as ERROR / DECLINED and
// end in a named Fatal there (RequireReadbackReplyComplete: "a blocking readback has no pixels
// to return"); this form has no reply to carry them, and a pack buffer left holding its old
// bytes is the same silently wrong answer. So the refusal latches here, under the same family
// words.
// Every number here is the peer's, and they are checked BEFORE the read, so a corrupt record
// costs no GPU work - and completely enough that LandReadbackInBuffer cannot meet a range it
// would have to refuse: every run it writes starts at or before the LAST row's offset and is
// no wider than the tight extent, and both are bounded here by what one MGPSubData can name
// (MGPipeSetSubDataBufferRange: an offset up to 2^31-1, a width up to 2^32-1). Whether the
// range fits the buffer is the applier's own gate, the one every buffer write meets.
static const char* ReadbackToBufferLayoutFault(const MG_Pipe::MGPReadbackToBuffer& record,
Uint64 bytesPerPixel) {
const MG_Pipe::MGPBox& box = record.Src.Box;
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
const Uint64 tightBytes = rowBytes * box.H * box.D; // the caller proved this fits
if (record.RowStride < rowBytes) return "the row stride is narrower than a tight row";
if (box.D > 1 && record.ImageStride < record.RowStride) return "the image stride is narrower than a row";
if (record.SwapGroup != 0 && record.SwapGroup != 2 && record.SwapGroup != 4 && record.SwapGroup != 8)
return "the swap group is not a byte group GL_PACK_SWAP_BYTES reverses (2, 4 or 8)";
constexpr Uint64 kMaxOffset = 0x7FFFFFFFull;
const Uint64 lastImage = box.D - 1;
const Uint64 lastRow = box.H - 1;
if (record.DstBase > kMaxOffset) return "the first row starts past 2^31-1";
if (lastImage != 0 && record.ImageStride > (kMaxOffset - record.DstBase) / lastImage)
return "the last image starts past 2^31-1";
const Uint64 imageBase = record.DstBase + lastImage * record.ImageStride;
if (lastRow != 0 && record.RowStride > (kMaxOffset - imageBase) / lastRow)
return "the last row starts past 2^31-1";
if (tightBytes > 0xFFFFFFFFull) return "the read is wider than one buffer write can name";
return nullptr;
}
Bool ServerVerbSink::LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight,
Uint64 bytesPerPixel) {
const MG_Pipe::MGPBox& box = record.Src.Box;
const Uint64 rowBytes = static_cast<Uint64>(box.W) * bytesPerPixel;
const Uint64 rows = box.H;
const Uint64 images = box.D;
const Uint64 tightBytes = rowBytes * rows * images;
if (record.SwapGroup != 0) {
const Uint64 group = record.SwapGroup;
for (Uint64 at = 0; at + group <= tightBytes; at += group) {
for (Uint64 i = 0; i < group / 2; ++i) std::swap(tight[at + i], tight[at + group - 1 - i]);
}
}
// ONE WRITE PER CONTIGUOUS RUN: the whole read when the layout is tight (the common
// case - a neutral pack state, or an image-packed read of whole images), one per image
// when only the rows are tight, one per row otherwise. The bytes between rows are the
// application's (GL_PACK_ROW_LENGTH / ALIGNMENT padding) and are never written.
const Bool rowsTight = record.RowStride == rowBytes;
const Bool imagesTight = images == 1 || record.ImageStride == rowBytes * rows;
const Uint64 runBytes = rowsTight ? (imagesTight ? tightBytes : rowBytes * rows) : rowBytes;
const Uint64 runsPerImage = rowsTight ? 1 : rows;
const Uint64 imageRuns = rowsTight && imagesTight ? 1 : images;
for (Uint64 z = 0; z < imageRuns; ++z) {
for (Uint64 r = 0; r < runsPerImage; ++r) {
// No overflow and no refusal below: ReadbackToBufferLayoutFault bounded the last
// offset and the widest run before the read.
const Uint64 offset = record.DstBase + z * record.ImageStride + r * record.RowStride;
MG_Pipe::MGPSubData write{};
write.Res = record.Dst;
write.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
// HasLiveHostWrites stays 0 and that is the client's fact, not a default: the
// client takes this form only for a pack buffer it does not map
// (TextureReadbackEmit.inc, PackBufferReadbackTarget), so "no live host writer" is
// what a glBufferSubData of the same bytes would have said.
if (!MG_Pipe::MGPipeSetSubDataBufferRange(write, offset, runBytes)) return false;
const Uint8* bytes = tight + (z * runsPerImage + r) * runBytes;
++m_bufferReadbackWrites;
// The acceptance is the applier's (a dead handle is a counted no-op, a range
// past the store a trip wire), so a refused write ends the landing here.
if (!MG_Pipe::MGPipeApplyResourceSubData(write, bytes, nullptr)) return false;
}
}
++m_bufferReadbacks;
return true;
}
Bool ServerVerbSink::OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
const MG_Pipe::MGPReadbackInfo& info = record.Src;
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(static_cast<GLenum>(info.Format)),
MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast<GLenum>(info.Type))));
// The reply form's shape checks and its PH-3 bound: the scratch is sized from the record,
// so the SERVER's own maxReplyBytes caps it exactly as it caps an answer - the client bands
// this form by the same number (PlanReadbackBands), so no correct client is refused by it.
// ONE latch site for every shape fault (PeerLatchTest's row names it once).
constexpr Uint64 kUint64Max = std::numeric_limits<Uint64>::max();
const char* fault = nullptr;
Uint64 tight = 0;
if (bpp == 0 || info.Box.W == 0 || info.Box.H == 0 || info.Box.D != 1 ||
info.Box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
info.Box.H > static_cast<Uint32>(std::numeric_limits<GLsizei>::max())) {
fault = "the read box or its format is invalid";
} else if (static_cast<Uint64>(info.Box.W) > kUint64Max / info.Box.H ||
static_cast<Uint64>(info.Box.W) * info.Box.H > kUint64Max / bpp) {
fault = "the tight extent overflows";
} else {
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * bpp;
if (m_maxReplyBytes == 0 || tight > m_maxReplyBytes ||
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
fault = "the tight extent is past this server's maxReplyBytes (PH-3)";
} else if (info.DstOffset != 0 || info.DstSize != tight) {
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
} else {
fault = ReadbackToBufferLayoutFault(record, bpp);
}
}
if (fault != nullptr) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ReadPixelsToBuffer.shape\"} %s", fault);
}
if (!ReadBoundFramebufferTight(info, tight)) {
return SessionLatch(MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixelsToBuffer\"} - the server "
"has no GL.ReadPixels; the pack buffer would keep bytes the application is "
"about to read as pixels, and there is no reply to carry the decline");
}
return LandReadbackInBuffer(record, m_readbackScratch.data(), bpp);
}
Bool ServerVerbSink::OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) {
const MG_Pipe::MGPReadbackInfo& info = record.Src;
const Uint64 bpp = static_cast<Uint64>(MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)));
const char* fault = nullptr;
Uint64 tight = 0;
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
fault = "the image extent is not a whole level's";
} else {
tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D) {
fault = "the tight extent overflows";
} else if (info.DstOffset != 0 || info.DstSize != tight) {
fault = "Src.DstOffset / Src.DstSize are not 0 / the tight extent";
} else {
fault = ReadbackToBufferLayoutFault(record, bpp);
}
}
if (fault != nullptr) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"GetTextureImageToBuffer.shape\"} %s", fault);
}
Vector<Uint8> bytes;
if (!ReadTextureImageTight(info, bytes) || bytes.size() != tight) {
return SessionLatch(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"GetTextureImageToBuffer\"} - the level "
"could not be read (%llu of %llu bytes); the reply form answers ERROR here, and "
"this form has no reply to carry it",
static_cast<unsigned long long>(bytes.size()),
static_cast<unsigned long long>(tight));
}
return LandReadbackInBuffer(record, bytes.data(), bpp);
}
// P8-F: A PROTOCOL GUARD, NOT AN UNMIGRATED VERB. This was P5b's server-side stub shape
// (`Fatal{UnmigratedVerb, ...}`, "ServerVerbSink has no body for it yet"), but no package is
// going to give these two multi-draw shapes a body, because our client can no longer send
// either: client index arrays become one owned element buffer before the record is built
// (EmitTables.cpp's multi-draw arm, OwnedDrawInputs.h clears kDrawHasUserIndices), and no GL
// entry point produces an instanced multi-draw. A record that carries one came from a
// foreign peer, so the family is the one every other malformed-record check uses.
//
// PH-1 (3): the shape is the PEER's (a multi-draw record it chose to send), so in an armed
// session child the refusal latches and the verb returns false; unarmed it still dies.
static Bool MultiDrawShapeLatch(const char* slot) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} a multi-draw "
"record with a shape this protocol does not carry: client indices travel as an owned "
"element buffer and no GL entry point produces an instanced multi-draw",
slot);
}
// P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
// carries the GL call verbatim (rule D) and this body reproduces the backend call the
// monolith makes for that shape, reading only the record and the backend's own
// barrier-pulled state; the twenty GL entry points collapse onto the arms below:
//
// kDrawIsIndirect arrays: DrawArraysIndirect (DrawCount 1, Stride 0) /
// MultiDrawArraysIndirect / MultiDrawArraysIndirectCount (a parameter
// buffer named); indexed: the three Elements twins
// NumDraws != 1 MultiDrawArrays / MultiDrawElementsBaseVertex, the two arrays
// rebuilt from the ranges into bounded locals (rule C)
// arrays, one range DrawArrays / DrawArraysInstanced / DrawArraysInstancedBaseInstance
// indexed, one range DrawElementsBaseVertex (the P5 arm, unchanged, bias 0 for a plain
// DrawElements) / DrawRangeElements[BaseVertex] under
// kDrawHasIndexRange / the four DrawElementsInstanced* by whether a
// base vertex and a base instance are non-zero
// kDrawHasUserIndices the `indices` argument is the resolved SEG_STAGE run instead of an
// element-buffer offset (the client staged a client index array)
//
// "Instanced" is InstanceCount != 1 || StartInstance != 0: an instanced call with a count
// of 1 and no base instance is the plain draw it is equivalent to, and a count of 0 must
// NOT collapse onto the plain draw (it draws nothing, the plain draw would draw once).
//
// What is still refused by name, as protocol corruption (P8-F): a multi-draw that arrived
// with a span (our client stages client indices into an owned element buffer and never sets
// kDrawHasUserIndices) and a multi-draw that claims instancing (no GL entry point produces
// one; the client never sends it). Only a foreign peer reaches either.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
// COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED.
//
// Everything the texture and sampler families do per draw now reads
// [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount).
// Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a
// sync of a texture the draw is about to sample, or leaves an earlier draw's sampler
// object on a unit - and a decline is exactly what run-ahead cannot take, because the
// client has already moved on and there is no wait in which to notice.
//
// A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns
// the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 /
// Count=maxTouched+1) and the server's job is to check the promise, not to invent a
// second authority for it. So this is a CHECK and its failure is corruption.
//
// WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by
// set_context_values, which precedes the verb on the ring - the same ordering §2.1's
// XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record
// put there and not client memory (rule F).
//
// WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units;
// only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all
// (mark 0 with nothing ever touched) is the correct and only possible emission, so a
// count of 0 is admitted exactly while the applied counters say no unit was touched.
//
// THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler
// subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle
// frontend walk, and there is no window to check because there is no window. That is
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
// (a narrowed window carries a non-zero count that is merely too small). The moment
// either set has been received, the rule binds.
//
// PH-1 (3): both windows are the PEER's records, so the refusals latch in an armed
// session child (false = latched, the verb declines); unarmed they still die.
Bool CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return true;
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
if (maxTouched < 0) return true;
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
"units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops "
"the sync of at least one texture this draw samples",
verb, static_cast<int>(maxTouched), required, st.SamplerViewStart,
st.SamplerViewCount);
}
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
"units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves "
"an earlier draw's sampler object on at least one unit",
verb, static_cast<int>(maxTouched), required, st.SamplerStateStart,
st.SamplerStateCount);
}
return true;
}
} // namespace
#endif
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) {
if (userIndices != nullptr && !Wire::CheckDrawUserIndices(info, ranges, *userIndices)) {
return false; // PH-1 (3): latched (armed session child); unarmed it died inside
}
// The witness first, before the backend is consulted, so a unit process with no
// backend object still sees the wire's fields (PipeApplier.h LastDraw).
m_lastDraw = LastDrawRecord{};
m_lastDraw.Info = info;
if (ranges != nullptr && info.NumDraws != 0) m_lastDraw.FirstRange = ranges[0];
if (userIndices != nullptr) {
m_lastDraw.HadUserIndices = true;
m_lastDraw.UserIndexBytes = userIndices->Size;
}
if (indirect != nullptr) {
m_lastDraw.HadIndirect = true;
m_lastDraw.Indirect = *indirect;
}
++m_drawRecords;
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
// asked to resolve anything out of the windows.
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo")) return false;
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
const auto mode = static_cast<GLenum>(info.Mode);
GLenum indexType = 0;
switch (info.IndexSize) {
case 0: break; // arrays
case 1: indexType = GL_UNSIGNED_BYTE; break;
case 2: indexType = GL_UNSIGNED_SHORT; break;
case 4: indexType = GL_UNSIGNED_INT; break;
default:
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h) and nothing else is a
// legal width; defaulting to 4 would read past the element buffer.
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
}
// ---- the indirect family: the block is the whole description --------------------
if (indirect != nullptr) {
// The layout already refused a record that sets both flags or declares ranges
// beside the block, so NumDraws is 0 and there is no span here.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): the command/parameter buffer handles cross as the
// verb's own state; the backend's indirect arm resolves the buffer twins from
// them instead of reading the client's GL_DRAW_INDIRECT_BUFFER binding slot.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbIndirectBuffer = indirect->Buffer;
applierState.VerbIndirectParameterBuffer = indirect->ParameterBuffer;
#endif
const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
const auto stride = static_cast<GLsizei>(indirect->Stride);
const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer);
const auto parameterOffset = static_cast<GLintptr>(indirect->ParameterOffset);
// glMultiDraw*Indirect with drawcount 1 and stride 0 IS glDraw*Indirect by GL's own
// definition, so the single-draw entry point is the one the monolith reaches for
// the single-draw call and nothing is lost for the multi-draw spelling of it.
const Bool single = !counted && drawCount == 1 && stride == 0;
if (info.IndexSize == 0) {
if (counted) {
if (gl.MultiDrawArraysIndirectCount == nullptr) return false;
gl.MultiDrawArraysIndirectCount(mode, offset, parameterOffset, drawCount, stride);
} else if (single) {
if (gl.DrawArraysIndirect == nullptr) return false;
gl.DrawArraysIndirect(mode, offset);
} else {
if (gl.MultiDrawArraysIndirect == nullptr) return false;
gl.MultiDrawArraysIndirect(mode, offset, drawCount, stride);
}
} else {
if (counted) {
if (gl.MultiDrawElementsIndirectCount == nullptr) return false;
gl.MultiDrawElementsIndirectCount(mode, indexType, offset, parameterOffset,
drawCount, stride);
} else if (single) {
if (gl.DrawElementsIndirect == nullptr) return false;
gl.DrawElementsIndirect(mode, indexType, offset);
} else {
if (gl.MultiDrawElementsIndirect == nullptr) return false;
gl.MultiDrawElementsIndirect(mode, indexType, offset, drawCount, stride);
}
}
++m_draws;
return true;
}
if (ranges == nullptr || info.NumDraws == 0) return false;
const Bool instanced = info.InstanceCount != 1 || info.StartInstance != 0;
const auto instanceCount = static_cast<GLsizei>(info.InstanceCount);
const GLuint baseInstance = info.StartInstance;
// ---- the multi-draws: the two arrays rebuilt from the ranges (rule C) --------------
if (info.NumDraws != 1) {
if (userIndices != nullptr) {
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+CLIENT_INDICES"
: "MultiDrawElements+CLIENT_INDICES");
}
if (instanced) {
return MultiDrawShapeLatch(info.IndexSize == 0 ? "MultiDrawArrays+INSTANCED"
: "MultiDrawElements+INSTANCED");
}
const auto n = static_cast<SizeT>(info.NumDraws);
m_multiCounts.resize(n);
if (info.IndexSize == 0) {
if (gl.MultiDrawArrays == nullptr) return false;
m_multiFirsts.resize(n);
for (SizeT i = 0; i < n; ++i) {
m_multiFirsts[i] = static_cast<GLint>(ranges[i].Start);
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
}
gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(),
static_cast<GLsizei>(n));
} else {
if (gl.MultiDrawElementsBaseVertex == nullptr) return false;
m_multiOffsets.resize(n);
m_multiBaseVertices.resize(n);
for (SizeT i = 0; i < n; ++i) {
m_multiOffsets[i] = reinterpret_cast<const void*>(
static_cast<std::uintptr_t>(ranges[i].Start) * info.IndexSize);
m_multiCounts[i] = static_cast<GLsizei>(ranges[i].Count);
m_multiBaseVertices[i] = ranges[i].IndexBias;
}
// MultiDrawElements is the same call with every base vertex 0, which is what
// its ranges carry (CONTRACT-P5B.md d1's MultiDrawElements row).
gl.MultiDrawElementsBaseVertex(mode, m_multiCounts.data(), indexType,
m_multiOffsets.data(), static_cast<GLsizei>(n),
m_multiBaseVertices.data());
}
++m_draws;
return true;
}
// ---- one range ----------------------------------------------------------------------
const MG_Pipe::MGPDrawRange& range = ranges[0];
const auto count = static_cast<GLsizei>(range.Count);
if (info.IndexSize == 0) {
if (!instanced) {
if (gl.DrawArrays == nullptr) return false;
gl.DrawArrays(mode, static_cast<GLint>(range.Start), count);
} else if (baseInstance != 0) {
if (gl.DrawArraysInstancedBaseInstance == nullptr) return false;
gl.DrawArraysInstancedBaseInstance(mode, static_cast<GLint>(range.Start), count,
instanceCount, baseInstance);
} else {
if (gl.DrawArraysInstanced == nullptr) return false;
gl.DrawArraysInstanced(mode, static_cast<GLint>(range.Start), count, instanceCount);
}
++m_draws;
return true;
}
// Indexed. `indices` is the element-buffer byte offset Start * IndexSize - the same
// arithmetic PipeFill's emitter inverted - or, for a client index array, the staged run
// resolved through the process resolver the server installed (rule C: the pointer is
// used for this call only). The codec proved the span lies inside SEG_STAGE (all four
// honesty arms), so a null here is the resolver missing, which is a wiring fault.
const void* indices = nullptr;
if (userIndices != nullptr) {
indices = MG_Pipe::MGPipeHostBytes(*userIndices);
if (indices == nullptr) {
Wire::WireProtocolFatal("DrawVbo.userIndices",
"the user-index span passed the codec's four honesty arms "
"but MGPipeHostBytes resolved it to null; the server's "
"segment resolver is not installed");
}
} else {
indices = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(range.Start) *
info.IndexSize);
}
const GLint baseVertex = range.IndexBias;
const Bool ranged = (info.Flags & MG_Pipe::kDrawHasIndexRange) != 0;
// A BASE VERTEX OF 0 IS THE PLAIN ENTRY POINT, NOT THE BaseVertex ONE WITH A 0 - d1's one
// ruling against CONTRACT-P5B.md's "DrawElementsBaseVertex(..., 0), the P5 arm,
// unchanged". Espryt's DrawElementsBaseVertex slot calls glDrawElementsBaseVertex, which
// is ES 3.2 / OES_draw_elements_base_vertex and is NOT loaded on an ES 3.1 provider
// (ANGLE on D3D11: "Failed to load GLES function: glDrawElementsBaseVertex" at bring-up),
// so the P5 arm dereferenced a null function pointer on every plain glDrawElements - the
// one call every Minecraft frame is made of - while the monolith, which calls
// GL.DrawElements for glDrawElements, was fine. Rule D says the sink reproduces the call
// the monolith makes; this is that, for all three indexed families.
if (!instanced) {
if (ranged) {
if (baseVertex != 0) {
if (gl.DrawRangeElementsBaseVertex == nullptr) return false;
gl.DrawRangeElementsBaseVertex(mode, info.MinIndex, info.MaxIndex, count, indexType,
indices, baseVertex);
} else {
if (gl.DrawRangeElements == nullptr) return false;
gl.DrawRangeElements(mode, info.MinIndex, info.MaxIndex, count, indexType, indices);
}
} else if (baseVertex != 0) {
if (gl.DrawElementsBaseVertex == nullptr) return false;
gl.DrawElementsBaseVertex(mode, count, indexType, indices, baseVertex);
} else {
if (gl.DrawElements == nullptr) return false;
gl.DrawElements(mode, count, indexType, indices);
}
} else if (baseInstance != 0) {
if (baseVertex != 0) {
if (gl.DrawElementsInstancedBaseVertexBaseInstance == nullptr) return false;
gl.DrawElementsInstancedBaseVertexBaseInstance(mode, count, indexType, indices,
instanceCount, baseVertex, baseInstance);
} else {
if (gl.DrawElementsInstancedBaseInstance == nullptr) return false;
gl.DrawElementsInstancedBaseInstance(mode, count, indexType, indices, instanceCount,
baseInstance);
}
} else if (baseVertex != 0) {
if (gl.DrawElementsInstancedBaseVertex == nullptr) return false;
gl.DrawElementsInstancedBaseVertex(mode, count, indexType, indices, instanceCount, baseVertex);
} else {
if (gl.DrawElementsInstanced == nullptr) return false;
gl.DrawElementsInstanced(mode, count, indexType, indices, instanceCount);
}
++m_draws;
return true;
}
// -----------------------------------------------------------------------------------
// P5b: the stubs the four migration packages replace (MG_Remote/CONTRACT-P5B.md).
//
// Each dies by the GL slot's own name. The record has crossed and been validated by the
// codec by the time one of these runs, so the only thing missing is the backend call, and
// the package that owns the row writes it here: `Table("<row>")`, the null-slot check
// (a backend that leaves the slot null DECLINES, which is the monolith's null-slot answer
// in the same words), the call, and a tally the lane can assert moved.
// -----------------------------------------------------------------------------------
// ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
//
// RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the
// record and from server state, and NOTHING ELSE: the backend goes on reading the frontend
// fields it reads today through the BARRIER-PULLED entries of its verb class, which the
// verb stamp PipeApplier::ApplyOne put up before this sink ran is exactly what makes legal.
// That is why none of these touches a backend file and why the monolith path is byte
// identical - and it is also the honest statement of the debt, which `rsp` counts.
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
if (!CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid")) return false;
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
// The compute program is NOT named by THIS record and must not be - but it IS named, by
// the set_dispatch_program record that preceded it, and the backend's PrepareForCompute
// reads MGPipeApplier().DispatchProgram from there.
//
// P5e (pa): that is what this comment used to get wrong. Until P5e the backend PULLED
// GetProgramForDispatch - GetProgramForDraw's twin - inside PrepareForCompute, and the
// field's FATAL -> BARRIER_PULLED move in FieldOwnership.def (contract §6.9, i1's one
// granted row) is what made the pull legal. pg gave the sync and the link/SPIR-V gate a
// handle arm and pa retired the pull itself, so on the handle arm the row is not read at
// all; the BARRIER_PULLED class stays because the monolith arm still reads it (ID-81).
// Block* are 0 on the wire for the unchanged reason: the local size is a link artifact
// the backend reads from the program it resolved, which on that arm is the record's
// archive.
if (grid.IsIndirect != 0) {
if (gl.DispatchComputeIndirect == nullptr) return false;
// IndirectBuffer travels for P7's sake; the BINDING is server state, put there by
// the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo
// is not re-resolved here. glDispatchComputeIndirect takes only the offset.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): the buffer handle itself is now also the verb's own
// state, so the backend's dispatch-indirect arm resolves the twin from the record
// rather than from the client's GL_DISPATCH_INDIRECT_BUFFER binding slot.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbDispatchIndirectBuffer = grid.IndirectBuffer;
#endif
gl.DispatchComputeIndirect(static_cast<GLintptr>(grid.IndirectOffset));
} else {
if (gl.DispatchCompute == nullptr) return false;
gl.DispatchCompute(static_cast<GLuint>(grid.GridX), static_cast<GLuint>(grid.GridY),
static_cast<GLuint>(grid.GridZ));
}
++m_dispatches;
return true;
}
Bool ServerVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("memory_barrier");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
// THE BITS GO OVER VERBATIM AND ARE LOWERED HERE BY NOBODY. Espryt's atomic-counter
// lowering - the counter bit implying the storage bit, because glslang lowers every
// atomic_uint onto a storage block - lives inside its own MemoryBarrier
// (DirectGLES.cpp:8837) and is a statement about the DRIVER. Repeating it on this side
// would make the split arm and the monolith arm two different calls.
if (barrier.ByRegion != 0) {
if (gl.MemoryBarrierByRegion == nullptr) return false;
gl.MemoryBarrierByRegion(static_cast<GLbitfield>(barrier.Bits));
} else {
if (gl.MemoryBarrier == nullptr) return false;
gl.MemoryBarrier(static_cast<GLbitfield>(barrier.Bits));
}
++m_memoryBarriers;
return true;
}
Bool ServerVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resource_copy_region");
if (table == nullptr) return false;
if (table->GL.CopyImageSubData == nullptr) return false;
// Typed handles resolve both texture and renderbuffer storage on the server.
MG_Backend::CopyImageEndpoint src{};
MG_Backend::CopyImageEndpoint dst{};
if (copy.SrcTarget == GL_RENDERBUFFER) src.RenderbufferHandle = copy.Src;
else src.TextureHandle = copy.Src;
if (copy.DstTarget == GL_RENDERBUFFER) dst.RenderbufferHandle = copy.Dst;
else dst.TextureHandle = copy.Dst;
if (MG_Pipe::MGPipeHandleIsNull(copy.Src) || MG_Pipe::MGPipeHandleIsNull(copy.Dst)) {
// The monolith's own answer to this, in its own words (DirectGLES.cpp:9067
// "source or destination image failed to sync; declining the copy"): the frontend
// validator is what keeps it unreachable and what reports the INVALID_VALUE the
// application is owed. A decline here is a real answer, not a silent success.
MGLOG_E_ONCE("MG_Remote server: resource_copy_region named texture(s) %u -> %u that "
"the frontend no longer holds; declining the copy",
static_cast<unsigned>(copy.SrcGlName),
static_cast<unsigned>(copy.DstGlName));
return false;
}
// SrcBox is {origin, extent} and the extent is the copy's, spelled once by GL for both
// endpoints; the destination contributes only its origin.
table->GL.CopyImageSubData(src, static_cast<GLenum>(copy.SrcTarget),
static_cast<GLint>(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y,
copy.SrcBox.Z, dst, static_cast<GLenum>(copy.DstTarget),
static_cast<GLint>(copy.DstLevel), copy.DstX, copy.DstY,
copy.DstZ, static_cast<GLsizei>(copy.SrcBox.W),
static_cast<GLsizei>(copy.SrcBox.H),
static_cast<GLsizei>(copy.SrcBox.D));
++m_imageCopies;
return true;
}
Bool ServerVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_shader_image");
if (table == nullptr) return false;
if (table->GL.BindImageTexture == nullptr) return false;
// THE SAME CALL IS RIGHT FOR BOTH BACKENDS, which is why the record carries the whole
// argument list although neither reads all of it today: Espryt ignores everything but
// Unit and syncs that unit from the barrier-pulled GetImageTextureBinding
// (DirectGLES.cpp:9154, :2471), and Magma's slot is a no-op (DirectVulkan.cpp:665). The
// arguments travel because rule D says a verb crosses as the CALL, and because P7 is
// what makes the backend read them instead of pulling.
table->GL.BindImageTexture(static_cast<GLuint>(bind.Unit), static_cast<GLuint>(bind.GlName),
static_cast<GLint>(bind.Level),
bind.Layered != 0 ? GL_TRUE : GL_FALSE,
static_cast<GLint>(bind.Layer),
static_cast<GLenum>(bind.Access),
static_cast<GLenum>(bind.Format));
++m_imageBinds;
// P5c (rv): an image bind moves the frontend's texture bind generation, and this verb
// carries no set_shader_images alongside it - so the server-side shutter serial the
// accessors now answer with (CONTRACT-P5C.md §5.3) moves here, at the one place the
// event reaches the applier's side.
MG_Pipe::MGPipeApplierNoteTextureStateMoved();
return true;
}
Bool ServerVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
const char* name) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("set_storage_block_binding");
if (table == nullptr) return false;
if (table->GL.ShaderStorageBlockBinding == nullptr) return false;
if (name == nullptr) return false;
// The NAME is the one coordinate the application, the frontend and both backends agree
// on (BackendObject.h:216-221), which is why the row carries a blob rather than the
// application's block INDEX. `name` points into the decoder's bounded local and is
// valid for this call only (rule C); the backend slot copies what it needs.
//
// P5f fe: publish the record's program handle for the backend consumer. Espryt
// resolves its server twin directly; Magma's consumer is the fm package's seam.
MG_Pipe::MGPipeApplier().ClearVerbHandles();
MG_Pipe::MGPipeApplier().VerbStorageBlockProgram = binding.ShaderCso;
table->GL.ShaderStorageBlockBinding(static_cast<GLuint>(binding.GlName), name,
static_cast<GLuint>(binding.Binding));
++m_storageBlockBindings;
return true;
}
// ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
//
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
// everything the backend reads around it - the capture program, the capture-buffer
// bindings, the bound XFB object, the patch state - it reads from gPipeInputs through its
// verb class's BARRIER-PULLED fields, which the client filled at the call site and the
// verb barrier holds still (R-1). That is why none of these touches m_backend beyond
// Table() and why not one of them caches anything across records.
//
// A NULL SLOT DECLINES, AND THE DECLINE IS THE MONOLITH'S ANSWER IN THE SAME WORDS. Magma
// (DirectVulkan) registers NO XFB slot and no PatchParameteri at all
// (BackendObject_DirectVulkan.cpp), and under monolith the frontend's own
// `if (const auto f = table.GL.X)` guard simply skips the call; `return false` here is that
// same skip, reported to DecodeAndApply as "this build did not apply it" rather than as a
// crash or as a silent success. Contract §2 t2 says so for PatchParameteri by name.
Bool ServerVerbSink::OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) {
auto& state = MG_Pipe::MGPipeApplier();
state.BoundStreamOutputLifetimeId = begin.LifetimeId;
state.StreamOutputSpans[begin.LifetimeId] = begin;
const MG_Backend::GlobalBackendFunctionsTable* table = Table("begin_stream_output");
if (table == nullptr) return false;
if (table->GL.BeginTransformFeedback == nullptr) return false;
// Espryt's Begin only ARMS the span (DirectGLES.cpp:1212-1220: primitiveMode, pending,
// targets cleared); the driver glBeginTransformFeedback happens in the tail of the next
// PrepareForDraw (StartPendingTransformFeedback, :1224), where the capture program and
// the buffer bindings are read through the pulls. So this record's effect is not
// visible until a DRAW crosses - which is why an XFB scenario whose draw is still class
// C moves its first blocker to that draw rather than rendering.
table->GL.BeginTransformFeedback(static_cast<GLenum>(begin.PrimitiveMode));
++m_streamOutputSpans;
return true;
}
Bool ServerVerbSink::OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("end_stream_output");
if (table == nullptr) return false;
if (table->GL.EndTransformFeedback == nullptr) {
auto& state = MG_Pipe::MGPipeApplier();
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
return false;
}
// THE THREE ACCOUNTING FIELDS ARE NOT READ, AND THAT IS THE RULING RATHER THAN AN
// OMISSION. glEndTransformFeedback takes no arguments; the numbers are the CLIENT's own
// per-span accounting (contract §2 t2's companions row) and the client is where they are
// consumed - by the primitive queries and by the capture-capacity clamp. A server that
// second-guessed them from its own driver would be publishing a second answer to a
// question the frontend already answers, and the second answer is the one that goes
// stale. They cross because the row has carried them since P4a and because P9's
// server-side scatter is what will need them.
(void)accounting;
table->GL.EndTransformFeedback();
auto& state = MG_Pipe::MGPipeApplier();
state.StreamOutputSpans.erase(state.BoundStreamOutputLifetimeId);
++m_streamOutputSpans;
return true;
}
Bool ServerVerbSink::OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("pause_stream_output");
if (table == nullptr) return false;
if (table->GL.PauseTransformFeedback == nullptr) return false;
(void)control; // Reserved, and the contract says it is 0.
table->GL.PauseTransformFeedback();
++m_streamOutputControls;
return true;
}
Bool ServerVerbSink::OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("resume_stream_output");
if (table == nullptr) return false;
if (table->GL.ResumeTransformFeedback == nullptr) return false;
(void)control;
table->GL.ResumeTransformFeedback();
++m_streamOutputControls;
return true;
}
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
if (table == nullptr) return false;
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
// the ES object on first bind; passing the lifetime id would index a map that has never
// heard of it and silently create a second driver object per bind. The lifetime id
// identifies the P5f capture snapshot on this side. Keeping it separate preserves
// the backend GL-name argument while Begin/End find the right server-owned span.
MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId = bind.LifetimeId;
if (table->GL.BindTransformFeedback) table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
++m_streamOutputBinds;
return true;
}
Bool ServerVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) {
const auto* table = Table("DeleteTransformFeedback");
if (!table) return false;
if (!object.GlName || object.Pad0)
Wire::WireProtocolFatal("DeleteStreamOutput.name", "default object or reserved field");
auto& state = MG_Pipe::MGPipeApplier();
if (object.LifetimeId) {
state.StreamOutputSpans.erase(object.LifetimeId);
if (state.BoundStreamOutputLifetimeId == object.LifetimeId) state.BoundStreamOutputLifetimeId = 0;
}
state.VerbDeleteStreamOutputLifetimeId = object.LifetimeId;
if (table->GL.DeleteTransformFeedback) table->GL.DeleteTransformFeedback(object.GlName);
state.VerbDeleteStreamOutputLifetimeId = 0;
return true;
}
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
if (table == nullptr) return false;
// Magma registers no PatchParameteri: it compiles the patch size into its synthesized
// control stage from set_patch_state instead, so the DECLINE below is the whole of the
// right answer for that backend and not a gap (contract §2 t2).
if (table->GL.PatchParameteri == nullptr) return false;
// Pname is GL_PATCH_VERTICES and the frontend has already rejected every other spelling
// with INVALID_ENUM before the record was built, so this is a forward and not a switch.
table->GL.PatchParameteri(static_cast<GLenum>(patch.Pname), static_cast<GLint>(patch.Value));
++m_patchParameters;
return true;
}
// ---- f1 ----
Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
const auto* table = Table("GenerateMipmap");
if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.2): the texture the client resolved at Target on the active
// unit crosses as the verb's own state; the backend's mip-descriptor check resolves
// the record from it instead of probing the client allocator for the bound object's
// lifetime id (T2's mip half).
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbMipRes = plan.Res;
applierState.VerbMipBaseLevel = plan.BaseLevel;
applierState.VerbMipLevelCount = plan.LevelCount;
#endif
table->GL.GenerateMipmap(plan.Target);
return true;
}
Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
if (table == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.4): the destination texture the client resolved at the
// active unit crosses as the verb's own state; the backend resolves its twin from the
// handle instead of reading the client's texture-unit binding slot (T4).
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbCopyTexDst = copy.Dst;
#endif
if (copy.SubImage) {
if (table->GL.CopyTexSubImage2D == nullptr) return false;
table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset,
copy.X, copy.Y, copy.Width, copy.Height);
} else {
if (table->GL.CopyTexImage2D == nullptr) return false;
table->GL.CopyTexImage2D(copy.Target, copy.Level, copy.InternalFormat,
copy.X, copy.Y, copy.Width, copy.Height, 0);
}
return true;
}
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5) ------------------------------------------
//
// TWO CONTROL RECORDS, NO BACKEND TABLE AND NO DECLINE ARM. Neither body consults
// Table(): the reset belongs to the applier this process owns, and the death release
// belongs to the twin tables - a backend that registered no slots (Magma's XFB shape)
// still has an applier to reset and still answers a death with the same generation
// check. A record that cannot be proved is Fatal, not declined: both refusals are
// ProtocolCorruption because by the time the sink runs the codec has already proved the
// record's SHAPE, and what is left to check are the contract facts about the peer (§1).
Bool ServerVerbSink::OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
// §1: the serial is ASSERTED, never dispatched on. P5c has exactly one context per
// session, so the only legal sequence is 0, 1, 2, ... and the session's own count of
// accepted resets IS the expected value; anything else means the two ends disagree
// about how many make-current edges have crossed, which no backend answer can fix.
// PH-1 (3): latches in an armed session child (the peer wrote the serial); dies unarmed.
if (reset.ContextSerial != m_applierResetSerial) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the "
"record carries %llu and this session has accepted %llu reset(s); the "
"serial is asserted against the session's own count, not dispatched on "
"(one context per session in P5c)",
static_cast<unsigned long long>(reset.ContextSerial),
static_cast<unsigned long long>(m_applierResetSerial));
}
++m_applierResetSerial;
// THE WHOLE POINT OF THE RECORD: the reset runs HERE, on the apply thread, against
// the g_applier this role owns (PipeApply.cpp:409). The layer-2 guard inside
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
// call it replaced is the Fatal arm.
MG_Pipe::MGPipeApplierReset();
++m_applierResets;
return true;
}
// P14 S1. THE RING'S DOOR INTO THE SESSION'S CONTEXT TABLE.
//
// The record carries the client's current context token and this is where a session's
// attribution moves. It is deliberately NOT an assertion like OnApplierReset's serial: the
// client's EGL layer mints the token and the server has no second count to compare against,
// so what is checked is a FACT ABOUT THIS SESSION'S OWN TABLE - a token that CreateContext
// never created is corruption, not a late record. Token 0 is the release edge and is always
// legal (a session whose client released its context has no context, which is exactly the
// value a session that never bound one already answers).
Bool ServerVerbSink::OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) {
namespace SI = SharedImages;
ServerSession* session = ServerSession::Active();
if (session == nullptr) return false;
SI::SessionHolder& holder = session->SharedImageHolder();
const auto fill = [&reply](const SI::Image& image) {
reply.ImageId = image.Id;
reply.Modifier = image.Modifier;
reply.Width = image.Width;
reply.Height = image.Height;
reply.Format = image.Fourcc;
reply.Stride = image.Stride;
reply.Offset = image.Offset;
};
switch (op.Op) {
case MG_Pipe::kMGPSharedImageAllocate: {
std::string why;
SI::ImageRef image = SI::Allocate(op.Width, op.Height, op.Format, why);
if (image == nullptr) {
MGLOG_E("MG_Remote server: shared image %ux%u (fourcc 0x%08x) not allocated: %s", op.Width, op.Height,
op.Format, why.c_str());
return false;
}
Transport::ITransport* transport = session->ControlTransport();
// The descriptor goes out BEFORE the reply is posted, so the client finds it queued.
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, image->Id, 0};
const MobileGLResult shared = transport != nullptr
? transport->ShareFd(image->Fd, MobileGLByteSpan{&offer, sizeof(offer)})
: MOBILEGL_ERR_NOT_INITIALIZED;
if (shared != MOBILEGL_OK) {
MGLOG_E("MG_Remote server: shared image %llu could not be sent on the aux socket (rc=%d)",
static_cast<unsigned long long>(image->Id), static_cast<int>(shared));
return false;
}
holder.Hold(image);
fill(*image);
MGLOG_D("MG_Remote server: shared image %llu allocated, %ux%u stride %u",
static_cast<unsigned long long>(image->Id), image->Width, image->Height, image->Stride);
return true;
}
case MG_Pipe::kMGPSharedImageImport: {
int fd = -1;
std::string why;
if (session->T0Inbox().TakeSharedImageFd(seq, Transport::AdoptT0::kOfferWaitMs, &fd, why) !=
AdoptInbox::Outcome::Taken) {
MGLOG_E("MG_Remote server: shared-image import (record %llu) has no descriptor: %s",
static_cast<unsigned long long>(seq), why.c_str());
return false;
}
SI::ImageRef image = SI::Identify(fd, op.Width, op.Height, op.Format, why);
#if !defined(_WIN32)
::close(fd);
#endif
if (image == nullptr) {
MGLOG_W_ONCE("MG_Remote server: a dma-buf import was refused: %s", why.c_str());
return false;
}
if (image->Width != op.Width || image->Height != op.Height || !SI::FourccSupported(op.Format)) {
MGLOG_W("MG_Remote server: a dma-buf import of image %llu (%ux%u) as %ux%u fourcc 0x%08x was refused",
static_cast<unsigned long long>(image->Id), image->Width, image->Height, op.Width, op.Height,
op.Format);
return false;
}
holder.Hold(image);
fill(*image);
return true;
}
case MG_Pipe::kMGPSharedImageRelease:
reply.ImageId = op.ImageId;
return holder.Release(op.ImageId);
case MG_Pipe::kMGPSharedImagePresent: {
SI::ImageRef image = holder.Get(op.ImageId);
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
if (image == nullptr || backend == nullptr) return false;
MG_Backend::SharedImageView view;
view.Id = image->Id;
view.Width = image->Width;
view.Height = image->Height;
view.Fourcc = image->Fourcc;
view.NativeBuffer = image->Native;
if (!backend->BlitDefaultFramebufferToSharedImage(view, DamageOfRecord(op.DamageCount, op.Damage)))
return false;
fill(*image);
return true;
}
case MG_Pipe::kMGPSharedImageQueryBufferAge: {
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
if (backend == nullptr) return false;
reply.BufferAge = backend->QueryCurrentBufferAge((op.Format & MG_Pipe::kMGPBufferAgeDamageRegionFollows) != 0);
return true;
}
case MG_Pipe::kMGPSharedImageSetDamageRegion: {
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
if (backend == nullptr) return false;
return backend->SetCurrentDamageRegion(DamageOfRecord(op.DamageCount, op.Damage));
}
case MG_Pipe::kMGPSharedImageAttach: {
SI::ImageRef image = holder.Get(op.ImageId);
if (image == nullptr) return false;
if (!MG_Pipe::MGPipeApplyAttachSharedImage(op.Texture, image->Id)) {
MGLOG_E("MG_Remote server: shared image %llu could not be attached to texture {slot=%u, gen=%u}: the "
"texture has no record",
static_cast<unsigned long long>(image->Id), op.Texture.Slot, op.Texture.Gen);
return false;
}
fill(*image);
return true;
}
default:
MGLOG_E("MG_Remote server: shared_image operation %u is not one this server knows", op.Op);
return false;
}
}
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
// cannot cause: a bind is answered by the session's own table, so with no accepted
// session there is nothing to attribute the record to and nothing to refuse. (The window
// is Close() clearing the active session while the apply thread is still draining.)
ServerSession* session = ServerSession::Active();
if (session == nullptr) return false;
// PH-1 (3): the refusal latches in an armed session child (the peer wrote the token);
// unarmed it dies, like every other contract fact about a peer.
if (!session->BindContext(bind.ClientContextToken)) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"BindContext.ClientContextToken\"} - the "
"record binds token %llu, which this session never created; a bind_context "
"names a context CreateContext put in the session's table, and 0 (the "
"release) is the only token that needs no entry",
static_cast<unsigned long long>(bind.ClientContextToken));
}
++m_bindContexts;
#if MOBILEGL_BUILD_DISAGGREGATED
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
//
// `bind_context` is where the session's context attribution moves, and it is also the ONLY
// point at which the apply thread's native context may move: the MakeCurrent control frame
// that precedes it still carries the OLD context (the client emits the binding after the
// binding has taken effect, which is the ordering S1 chose on purpose), so a native switch
// driven from there would bind the context the client just left. Frontend GL state is the
// backend's per-tuple business; this call makes the tuple the resolver now names current.
//
// It is a no-op for the shapes S4 did not change: with no resolver installed (monolith, a
// unit case, the inproc one-process shape) the key is {0,0}, and the single-context world's
// bind does not move the native context - MakeNativeContextCurrentForBoundToken returns
// without a driver call when the context it resolves to is already the one bound here.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
(void)MG_Backend::DirectGLES::MakeNativeContextCurrentForBoundToken();
// The backend half for one that keeps a single native surface binding (Magma).
if (MG_Backend::BackendObject* backend = ServerLoopInstance().Backend()) {
backend->OnClientContextBound(bind.ClientContextToken);
}
}
#endif
return true;
}
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
// §1's zero ruling: a null handle means "the object never crossed", and the client
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
// not a no-op.
// PH-1 (3): both refusals latch in an armed session child; unarmed they die as before.
if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null "
"handle never crosses: the client emits nothing for an object its own "
"allocator cannot resolve (CONTRACT-P5C.md §5.2)");
}
if (death.Kind >= static_cast<Uint32>(MG_Pipe::MGPipeKind::KindCount)) {
return SessionLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
"MGPipeKind",
static_cast<unsigned>(death.Kind));
}
// The per-kind release, keyed by the handle the record carried. A false answer is
// NOT a decline: the kind's own delete opcode may already have released the twin
// (the idempotent second path every notice arm documents), and a kind this backend
// does not twin (Buffer, whose death crosses as resource_destroy) legally resolves
// to nothing.
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(
death.Handle, static_cast<MG_Pipe::MGPipeKind>(death.Kind));
++m_objectDeaths;
return true;
}
// -----------------------------------------------------------------------------------
// PipeApplier
// -----------------------------------------------------------------------------------
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
: m_segments(segments), m_replies(replies) {}
void PipeApplier::Attach(Transport::ILink* link, MG_Backend::BackendObject* backend) {
if (!link || !link->Attached() || !m_segments)
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, PipeApplier::Attach missing link}");
m_verbs.SetBackend(backend);
m_verbs.SetMaxReplyBytes(link->Capabilities().MaxReplyBytes);
m_decoder = Wire::PipeWireDecoder(link, m_segments, m_replies);
m_decoder.SetVerbSink(&m_verbs);
MG_Pipe::MGPipeServerBlockNoteIdentity(); m_attached = true;
}
void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) {
if (control == nullptr || m_segments == nullptr) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control "
"page or no segment table; ServerSession::Accept builds both before the "
"apply thread starts");
}
m_verbs.SetBackend(backend);
m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies);
m_decoder.SetVerbSink(&m_verbs);
// P5f (f1), CONTRACT-P5E §3.2: the server block's identity, once per session (the
// per-verb stamp refreshes it against the served-context serial). A no-op unless the
// dual-block rehearsal is armed; without it the server block's ContextIdentity() stays
// nullptr and the backend's identity-keyed memo caches read that as a HIT on their
// zero-initialised slot - an unnamed null dereference instead of a named marker.
MG_Pipe::MGPipeServerBlockNoteIdentity();
m_attached = true;
}
void PipeApplier::Detach() {
m_decoder = Wire::PipeWireDecoder();
m_verbs.SetBackend(nullptr);
m_attached = false;
}
Bool PipeApplier::Attached() const { return m_attached; }
Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) {
if (!m_attached) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} "
"- a record reached the applier with no decoder; the apply thread calls "
"Attach once before its first pop");
}
// R-1's INVARIANT, THE SERVER'S HALF (table 3's gPipeInputs row, c1-v1 8.1). The flag
// is raised for the WHOLE of this function and not only around DecodeAndApply: the
// stamp below and LeaveApplier at the end are both writes to gPipeInputs, and the client
// asserts the flag is down before it publishes (ClientSession::EmitAndWait), so a
// bracket that excluded either would leave a real write outside the check. It is
// dropped before this function returns, and s1's SessionConsumer::ApplyOne publishes
// appliedSeq only after that - so by the time the client is runnable the flag is down.
const Client::ClientSession::ScopedApplierEntry insideApplier;
// P5e (id), CONTRACT-P5E §2.1 / §4.4: STAMP WHETHER THE CLIENT IS PARKED BEHIND THIS
// RECORD, before anything can ask. It is the input to the allocator guard's exemption
// (SlotAllocator.cpp) and to every frontend-keyed twin member that survives as monolith
// glue (SlotTables.h), and it has to be up before DecodeAndApply because the sinks those
// reach are exactly the askers. MGPipeBarriered answers true for every record until ra
// lands the wait rule, so this line changes nothing this phase and is the line ra
// rebases onto rather than adds.
//
// AND THE STAMP IS `true` UNTIL ra LANDS THE CLIENT'S HALF (ID-103). The predicate
// describes what the client WILL do once EmitAndWaitTails follows the wait classes;
// today it still blocks after every record, so a `false` stamp here would withdraw the
// §4.4 exemptions from a probe the client's own wait still makes safe - a refusal with
// no defect behind it. The predicate is computed on every record all the same, so it is
// exercised for the whole phase rather than first run on the day it starts deciding.
//
// AND THE SERVER'S OWN CAPABILITY IS THE SECOND CONJUNCT (P5e gl, ID-111). The constant
// above says what the CLIENT will do once ra's wait rule is live; it says nothing about
// which server this is. kCapRunAheadApply is never published on DirectVulkan (ID-90,
// MGPipeRunAheadCapBitsFor), yet draw_vbo / blit / clear / launch_grid are all
// kWaitNone - so a stamp that read the build constant alone would, on the day it flips,
// have a MAGMA server mark every draw record UNBARRIERED while its client is still
// lockstep. CountBarrierPull (PipeInputs.cpp) is an unconditional Fatal on an
// unbarriered pull, no knob involved, and Magma's residual fill is its ONLY source for
// the seven pointer-backed fields: Magma would die on its first draw and take
// MagmaP7AllocatorDebtScope's exemption with it.
//
// BOTH PREDICATES ARE COMPUTED UNCONDITIONALLY, as arguments rather than as the arms of
// a short-circuit, which is ID-103's reason extended to the capability probe: they are
// exercised on every record for the whole phase rather than first running on the day
// they start deciding.
//
// PH-1 (3): BUT NOT BEFORE THE RECORD HAS BEEN ADMITTED. MGPipeBarriered reads payload
// fields (a DrawVbo's MGPDrawInfo::Flags) and the stamps index per-opcode tables, so a
// record whose ring header is shorter than its own type - or names no opcode - would be
// read past its end here, before DecodeAndApply's pre-gate could refuse it. In an armed
// session child the pre-gate therefore runs FIRST: a refused record latches by name and
// is declined with nothing stamped (ServerLoopTest's short-record case reads the stamp).
// Unarmed it admits everything and the generated gate keeps its death, as before.
if (!m_decoder.AdmitOrDecline(record)) return false;
const Bool wireSaysBarriered = MG_Pipe::MGPipeBarriered(
static_cast<MG_Pipe::MGPWireOp>(record.kind), record.payload, MG_Pipe::MGPipeApplier());
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(
MGPipeApplierStampsBarriered(MG_Pipe::kMGPipeP5eClientWaitRuleLanded,
MGPipeServerPublishesRunAhead(), wireSaysBarriered));
// P5e (gl), ID-128: AND WHETHER IT WAS BARRIERED BY ESCALATION RATHER THAN BY ITS CLASS.
// The predicate above is the static wait class plus two payload-derived escalations
// (ID-83: an open transform-feedback span, a draw carrying client vertex arrays), so the
// difference between it and the table IS the escalation - both halves are already
// computed here, and the second flag costs one compare. The strict knob is the only
// reader: a pull on an escalated record is a debt the phase that owns XFB (§5.7) or
// client arrays (ID-82) owes, neither of which is this one.
MG_Pipe::MGPipeApplierSetCurrentRecordBarrieredByEscalation(
wireSaysBarriered &&
MG_Pipe::MGPipeWaitClassFor(static_cast<MG_Pipe::MGPWireOp>(record.kind)) ==
MG_Pipe::kWaitNone);
// ORDER IS THE CONTRACT'S: stamp, then apply. The stamp is what makes any server-side
// read of gPipeInputs legal at all (PipeApplier.h's block 1), so a record applied
// before it aborts on the FIRST field inside SyncRenderState.
StampVerbBoundary(static_cast<MG_Pipe::MGPWireOp>(record.kind));
const Bool applied = m_decoder.DecodeAndApply(record);
// AND THE CLEAR IS INSIDE ApplyOne, NOT AFTER THE DRAIN BATCH. That is not tidiness,
// it is the barrier invariant. s1's SessionConsumer::ApplyOne publishes appliedSeq the
// instant this returns, and publishing appliedSeq is what makes the CLIENT runnable
// again (R-1: the barrier waits on exactly that watermark). A clear that ran after the
// batch would therefore be a second writer of gPipeInputs while the client is already
// touching it - the one thing table 3 says may not be introduced before the barrier
// retires - and the first version of this file had it there. It was caught by
// AClearRecordCrossesAndIsStampedAsAVerbBoundary failing INTERMITTENTLY, which is what
// a race looks like from the outside.
//
// THE COST, STATED: a record that is NOT a verb boundary now applies with the flag
// disarmed, so a sticky forward pulled from inside such a record's applier is not
// counted in `rsp`. Closing that needs an "enter the applier" entry point beside
// MGPipeServerStampVerbBoundary that arms the flag WITHOUT re-stamping - re-stamping on
// a non-verb op is what p1 forbids outright - and PipeInputs.cpp is p1's file. Left for
// the integrator to sequence; it makes `rsp` larger, never smaller, so the number this
// phase reports is a floor.
LeaveApplier();
return applied;
}
// p1's rule verbatim (p1-v1 2). MGPipeVerbForWireOp is generated from MGP_VERB_OP_LIST in
// FieldOwnership.def and answers kVerbCount for every op that is NOT a verb boundary, so
// calling it unconditionally on every record is both correct and cheap. Four ops stamp:
// Clear -> Clear, DrawVbo -> DrawArrays, ReadPixels -> ReadPixels, Blit -> BlitFramebuffer.
//
// PRESENT IS DELIBERATELY NOT ONE, although contract 7 puts it in class B: FillPoints.def:21
// says Present and SetSwapInterval "go through BackendObject virtuals and read no frontend
// state, so they are not verbs here". There is no MGPipeVerb::Present, and stamping there
// would retire the previous verb's answers with nothing to put in their place.
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp op) {
const MG_Pipe::MGPipeVerb verb = MG_Pipe::MGPipeVerbForWireOp(op);
if (verb == MG_Pipe::MGPipeVerb::kVerbCount) return; // not a verb boundary: stamp nothing
MG_Pipe::MGPipeServerStampVerbBoundary(verb);
}
void PipeApplier::LeaveApplier() { MG_Pipe::MGPipeServerClearVerbBoundary(); }
Uint64 PipeApplier::ResidualPullCount() const { return MG_Pipe::MGPipeResidualPullCount(); }
// The decoder poisons EXACTLY the runs it resolved, from inside DecodeAndApply, once the
// applier has returned - so this entry point is the manual one, for a caller that knows a
// range is dead and is not the decoder. It is kept because c0's signature block declares
// it and because the R-11 copy in Managers.cpp is verified by poisoning a range by hand in
// a unit case; nothing on the live path calls it.
void PipeApplier::PoisonRetiredStageBytes(Uint64 offset, Uint64 size) {
if (size == 0 || m_segments == nullptr) return;
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MG_Config::Ipc.Audit) return;
#endif
const void* run = m_segments->Resolve(Wire::kSegStage, offset, size);
if (run == nullptr) return;
std::memset(const_cast<void*>(run), 0xDD, static_cast<SizeT>(size));
}
Uint64 PipeApplier::PoisonedStageBytes() const { return m_decoder.PoisonedStageBytes(); }
Uint64 PipeApplier::DecoderAppliedSeq() const { return m_decoder.AppliedSeq(); }
} // namespace MobileGL::MG_Remote::Server