diff --git a/CMakeLists.txt b/CMakeLists.txt index 73c81954..f97d3fe3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -697,6 +697,8 @@ if (MOBILEGL_BUILD_RECORD_ARM) MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp # P5 b1: the block-granularity persistent-map push the record arm makes mandatory (T2). MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp + # The record verbs' consumer (ServerVerbSink under a transport is this plus the session's). + MobileGL/MG_Backend/Record/RecordVerbSink.cpp ) endif() diff --git a/MobileGL/MG_Backend/Record/RecordVerbSink.cpp b/MobileGL/MG_Backend/Record/RecordVerbSink.cpp new file mode 100644 index 00000000..67d4335b --- /dev/null +++ b/MobileGL/MG_Backend/Record/RecordVerbSink.cpp @@ -0,0 +1,1157 @@ +// MobileGL - MobileGL/MG_Backend/Record/RecordVerbSink.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 + +// P13 W5: the record verbs' server half, moved verbatim from MG_Remote/Server/PipeApplier.cpp +// (ServerVerbSink) so a library without a transport applies them too. Deaths and latches go +// through MG_Pipe's record seam (PipeSessionFail.h), which is SessionFail / SessionLatch wherever +// MG_Remote is linked; the wire's user-index span is checked by the subclass that has a wire. + +#include "RecordVerbSink.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace MobileGL::MG_Record { + void RecordVerbSink::SetBackend(MG_Backend::BackendObject* backend) { + if (m_backend != backend) OnBackendChanged(); + m_backend = backend; + if (backend != nullptr) { + const auto& limits = backend->GetDynamicParameters(); + MG_Record::ServerStagedTexture().SetDeviceLimits( + limits.MaxTextureSize, limits.Max3DTextureSize, limits.MaxCubeMapTextureSize, + limits.MaxArrayTextureLayers, limits.MaxTextureBufferSize); + } + } + + const MG_Backend::GlobalBackendFunctionsTable* RecordVerbSink::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(); + } + + Bool RecordVerbSink::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)) + MG_Pipe::MGPipeProtocolFatal("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(clear.BufferMask)); + break; + case kMGPClearKindColor: + switch (clear.ValueClass) { + case MG_Pipe::kMGPipeClearValueClassFloat: + if (gl.ClearBufferfv == nullptr) return false; + gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(clear.ColorValue)); + break; + case MG_Pipe::kMGPipeClearValueClassInt: + if (gl.ClearBufferiv == nullptr) return false; + gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(clear.ColorValue)); + break; + case MG_Pipe::kMGPipeClearValueClassUint: + if (gl.ClearBufferuiv == nullptr) return false; + gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex, + reinterpret_cast(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. + MG_Pipe::MGPipeProtocolFatalAt("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: + MG_Pipe::MGPipeProtocolFatalAt("MGPClear::Kind", clear.Kind, kMGPClearKindDepthStencil + 1); + } + ++m_clears; + return true; + } + + Bool RecordVerbSink::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(blit.Mask), + static_cast(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; + } + + Bool RecordVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + MG_Pipe::MGPipeReplySink* replies) { + if (!replies) MG_Pipe::MGPipeProtocolFatal("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) { + MG_Pipe::MGPipeProtocolFatal("GetTextureImage.extent", "invalid tight image extent"); + } + const Uint64 tight = static_cast(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) + MG_Pipe::MGPipeProtocolFatal("GetTextureImage.range", "invalid reply window"); + auto image = info; + image.DstOffset = 0; + image.DstSize = tight; + Vector bytes; + const Bool ok = ReadTextureImageTight(image, bytes); + if (!ok || bytes.size() != tight) { + replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusError, nullptr, 0); + return false; + } + replies->PostReply(seq, MG_Pipe::MGPipeReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize); + m_readbackBytes += info.DstSize; + ++m_readbacks; + return true; + } + + Bool RecordVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + MG_Pipe::MGPipeReplySink* 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. + MG_Pipe::MGPipeRecordFail(MG_Pipe::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(seq)); + } + const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel( + MG_Util::ConvertGLEnumToTextureInputFormat(static_cast(info.Format)), + MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast(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, MG_Pipe::MGPipeReplySink::kStatusError, nullptr, 0); + return false; + }; + if (bytesPerPixel == 0 || info.Box.W == 0 || info.Box.H == 0 || + info.Box.W > static_cast(std::numeric_limits::max()) || + info.Box.H > static_cast(std::numeric_limits::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(bytesPerPixel); + constexpr Uint64 kUint64Max = std::numeric_limits::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(std::numeric_limits::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(info.DstSize), + static_cast(tight), info.Box.W, info.Box.H, + static_cast(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, MG_Pipe::MGPipeReplySink::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, MG_Pipe::MGPipeReplySink::kStatusOk, m_readbackScratch.data(), tight); + m_readbackBytes += tight; + ++m_readbacks; + return true; + } + + Bool RecordVerbSink::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(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 + // gPipeInputs->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(info.Box.W), + static_cast(info.Box.H), static_cast(info.Format), + static_cast(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 !MG_Pipe::MGPipeRecordLatched(); + } + + Bool RecordVerbSink::ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector& 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(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 RecordVerbSink::LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight, + Uint64 bytesPerPixel) { + const MG_Pipe::MGPBox& box = record.Src.Box; + const Uint64 rowBytes = static_cast(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. + const MG_Pipe::MGPipeServerOriginatedWriteScope serverWrite; + if (!MG_Pipe::MGPipeApplyResourceSubData(write, bytes, nullptr)) return false; + } + } + ++m_bufferReadbacks; + return true; + } + + Bool RecordVerbSink::OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) { + const MG_Pipe::MGPReadbackInfo& info = record.Src; + const Uint64 bpp = static_cast(MG_Util::GetInputBytesPerPixel( + MG_Util::ConvertGLEnumToTextureInputFormat(static_cast(info.Format)), + MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast(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::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(std::numeric_limits::max()) || + info.Box.H > static_cast(std::numeric_limits::max())) { + fault = "the read box or its format is invalid"; + } else if (static_cast(info.Box.W) > kUint64Max / info.Box.H || + static_cast(info.Box.W) * info.Box.H > kUint64Max / bpp) { + fault = "the tight extent overflows"; + } else { + tight = static_cast(info.Box.W) * info.Box.H * bpp; + if (m_maxReplyBytes == 0 || tight > m_maxReplyBytes || + tight > static_cast(std::numeric_limits::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 MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ReadPixelsToBuffer.shape\"} %s", fault); + } + if (!ReadBoundFramebufferTight(info, tight)) { + return MG_Pipe::MGPipeRecordLatch(MG_Pipe::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 RecordVerbSink::OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) { + const MG_Pipe::MGPReadbackInfo& info = record.Src; + const Uint64 bpp = static_cast(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(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 MG_Pipe::MGPipeRecordLatch(MG_Pipe::MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"GetTextureImageToBuffer.shape\"} %s", fault); + } + Vector bytes; + if (!ReadTextureImageTight(info, bytes) || bytes.size() != tight) { + return MG_Pipe::MGPipeRecordLatch(MG_Pipe::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(bytes.size()), + static_cast(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 MG_Pipe::MGPipeRecordLatch(MG_Pipe::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(maxTouched) + 1u; + if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) { + return MG_Pipe::MGPipeRecordLatch(MG_Pipe::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(maxTouched), required, st.SamplerViewStart, + st.SamplerViewCount); + } + if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) { + return MG_Pipe::MGPipeRecordLatch(MG_Pipe::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(maxTouched), required, st.SamplerStateStart, + st.SamplerStateCount); + } + return true; + } + } // namespace +#endif + + Bool RecordVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, + const MG_Pipe::MGPDrawRange* ranges, + const MG_Pipe::MGHostSpan* userIndices, + const MG_Pipe::MGPDrawIndirect* indirect) { + if (userIndices != nullptr && !CheckUserIndices(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(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. + MG_Pipe::MGPipeProtocolFatalAt("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(static_cast(indirect->Offset)); + const auto drawCount = static_cast(indirect->DrawCount); + const auto stride = static_cast(indirect->Stride); + const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer); + const auto parameterOffset = static_cast(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(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(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(ranges[i].Start); + m_multiCounts[i] = static_cast(ranges[i].Count); + } + gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(), + static_cast(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( + static_cast(ranges[i].Start) * info.IndexSize); + m_multiCounts[i] = static_cast(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(n), + m_multiBaseVertices.data()); + } + ++m_draws; + return true; + } + + // ---- one range ---------------------------------------------------------------------- + const MG_Pipe::MGPDrawRange& range = ranges[0]; + const auto count = static_cast(range.Count); + if (info.IndexSize == 0) { + if (!instanced) { + if (gl.DrawArrays == nullptr) return false; + gl.DrawArrays(mode, static_cast(range.Start), count); + } else if (baseInstance != 0) { + if (gl.DrawArraysInstancedBaseInstance == nullptr) return false; + gl.DrawArraysInstancedBaseInstance(mode, static_cast(range.Start), count, + instanceCount, baseInstance); + } else { + if (gl.DrawArraysInstanced == nullptr) return false; + gl.DrawArraysInstanced(mode, static_cast(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) { + MG_Pipe::MGPipeProtocolFatal("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(static_cast(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("")`, 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 RecordVerbSink::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(grid.IndirectOffset)); + } else { + if (gl.DispatchCompute == nullptr) return false; + gl.DispatchCompute(static_cast(grid.GridX), static_cast(grid.GridY), + static_cast(grid.GridZ)); + } + ++m_dispatches; + return true; + } + + Bool RecordVerbSink::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(barrier.Bits)); + } else { + if (gl.MemoryBarrier == nullptr) return false; + gl.MemoryBarrier(static_cast(barrier.Bits)); + } + ++m_memoryBarriers; + return true; + } + + Bool RecordVerbSink::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(copy.SrcGlName), + static_cast(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(copy.SrcTarget), + static_cast(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y, + copy.SrcBox.Z, dst, static_cast(copy.DstTarget), + static_cast(copy.DstLevel), copy.DstX, copy.DstY, + copy.DstZ, static_cast(copy.SrcBox.W), + static_cast(copy.SrcBox.H), + static_cast(copy.SrcBox.D)); + ++m_imageCopies; + return true; + } + + Bool RecordVerbSink::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(bind.Unit), static_cast(bind.GlName), + static_cast(bind.Level), + bind.Layered != 0 ? GL_TRUE : GL_FALSE, + static_cast(bind.Layer), + static_cast(bind.Access), + static_cast(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 RecordVerbSink::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(binding.GlName), name, + static_cast(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 RecordVerbSink::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(begin.PrimitiveMode)); + ++m_streamOutputSpans; + return true; + } + + Bool RecordVerbSink::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 RecordVerbSink::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 RecordVerbSink::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 RecordVerbSink::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(bind.GlName)); + ++m_streamOutputBinds; + return true; + } + + Bool RecordVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) { + const auto* table = Table("DeleteTransformFeedback"); + if (!table) return false; + if (!object.GlName || object.Pad0) + MG_Pipe::MGPipeProtocolFatal("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 RecordVerbSink::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(patch.Pname), static_cast(patch.Value)); + ++m_patchParameters; + return true; + } + + // ---- f1 ---- + Bool RecordVerbSink::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 RecordVerbSink::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; + } + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_Backend/Record/RecordVerbSink.h b/MobileGL/MG_Backend/Record/RecordVerbSink.h new file mode 100644 index 00000000..3c30e2ff --- /dev/null +++ b/MobileGL/MG_Backend/Record/RecordVerbSink.h @@ -0,0 +1,197 @@ +// MobileGL - MobileGL/MG_Backend/Record/RecordVerbSink.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P13 W5: THE RECORD VERBS' CONSUMER. The verbs no MGPipeApply* entry point takes (clear, blit, the +// readbacks, the draws, dispatch, copies, image binds, XFB, mipmaps) end in the backend's own call +// through the private GlobalBackendFunctionsTable this sink holds. Under a transport the server's +// ServerVerbSink (MG_Remote/Server/PipeApplier.h) is this plus the session's verbs; the record +// arm's in-process port drives this class directly, which is what lets a library without a +// transport run the record arm. + +#pragma once + +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Record { + + class RecordVerbSink : public MG_Pipe::MGPipeVerbSink { + public: + // The server's private backend. Null until ServerLoop::CreateBackend has run, and a + // verb that arrives before then declines by name rather than dereferencing. + void SetBackend(MG_Backend::BackendObject* backend); + MG_Backend::BackendObject* Backend() const { return m_backend; } + void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; } + + Bool OnClear(const MG_Pipe::MGPClear& clear) override; + Bool OnBlit(const MG_Pipe::MGPBlit& blit) override; + Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + MG_Pipe::MGPipeReplySink* replies) override; + Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + MG_Pipe::MGPipeReplySink* replies) override; + // P9 (CONTRACT-P9.md §1): the pack-buffer halves. Nothing is answered; the tight pixels + // land in record.Dst through the applier's buffer-write gate. + Bool OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override; + Bool OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override; + Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, + const MG_Pipe::MGHostSpan* userIndices, + const MG_Pipe::MGPDrawIndirect* indirect) override; + + // ---- P5b (MG_Remote/CONTRACT-P5B.md): one override per row a migration package owns. + // At the contract commit EVERY BODY BELOW IS A STUB that dies + // Fatal{UnmigratedVerb, ""} by the slot's own name - the same line the client's + // class-C table raises and the census greps - so a client flipped ahead of its server + // half aborts by name rather than rendering nothing, and the census on this head is + // unchanged (the client refuses first). The owning package replaces the body. + // + // i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion + // ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"), + // OnSetStorageBlockBinding ("ShaderStorageBlockBinding") + // t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput / + // OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies + // now: the backend call the contract names, the null-slot DECLINE, a tally. + // f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" / + // "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already) + // d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the + // instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots + // produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names) + Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override; + Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override; + Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override; + Bool OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) override; + Bool OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding, + const char* name) override; + Bool OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) override; + Bool OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) override; + Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; + Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; + Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override; + Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override; + Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override; + Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override; + Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override; + + // Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a + // split build is also what a scenario that ran entirely on the monolith path looks + // like (R-16: a probe may not arm against a stub). + Uint64 Clears() const { return m_clears; } + Uint64 Draws() const { return m_draws; } + Uint64 Readbacks() const { return m_readbacks; } + Uint64 Blits() const { return m_blits; } + Uint64 ReadbackBytes() const { return m_readbackBytes; } + // P9: pack-buffer readbacks that landed in their buffer (both ops), and the buffer-write + // records they took. Readbacks() above counts only the reply-slot form. + Uint64 BufferReadbacks() const { return m_bufferReadbacks; } + Uint64 BufferReadbackWrites() const { return m_bufferReadbackWrites; } + // ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a + // split build "the scenario passed" is also what a scenario that never left the + // monolith path looks like - so the lane asserts the number that only this sink can + // move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry + // points and memory_barrier both barrier ones, and the sink is where they separate. + Uint64 ImageBinds() const { return m_imageBinds; } + Uint64 Dispatches() const { return m_dispatches; } + Uint64 MemoryBarriers() const { return m_memoryBarriers; } + Uint64 ImageCopies() const { return m_imageCopies; } + Uint64 StorageBlockBindings() const { return m_storageBlockBindings; } + // ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal + // the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from + // DstSize is exactly the heap overflow codex 1 found, one field over. + Uint64 ReadbackScratchBytes() const { return static_cast(m_readbackScratch.size()); } + + // ---- P5b d1: what the LAST draw_vbo record carried, as the sink saw it --------------- + // + // Recorded BEFORE the backend is consulted, so a process with no backend object (every + // unit case) can still assert the wire's fields rather than only that a draw "was + // declined": the record's head, its first range, its indirect block, and whether a + // user-index span rode with it and how many bytes it named. Nothing here outlives the + // call except these copies (rule C: the pointers the sink was handed are not kept). + struct LastDrawRecord { + MG_Pipe::MGPDrawInfo Info{}; + MG_Pipe::MGPDrawRange FirstRange{}; + MG_Pipe::MGPDrawIndirect Indirect{}; + Uint64 UserIndexBytes = 0; + Bool HadUserIndices = false; + Bool HadIndirect = false; + }; + const LastDrawRecord& LastDraw() const { return m_lastDraw; } + // draw_vbo records seen, applied or declined; Draws() above counts only the applied. + Uint64 DrawRecords() const { return m_drawRecords; } + // P5b t2's tallies, for the same reason the five above exist (R-16): under split "the + // scenario passed" is also what a scenario that ran entirely on the monolith path looks + // like, so a lane that wants to say the XFB spans CROSSED has to read a counter the + // server moved. Spans counts Begin and End together - they are one span and a lane that + // saw only one of them has a bug the two-counter version would have hidden behind a + // sum; controls counts Pause and Resume; binds and patch parameters count their own. + Uint64 StreamOutputSpans() const { return m_streamOutputSpans; } + Uint64 StreamOutputControls() const { return m_streamOutputControls; } + Uint64 StreamOutputBinds() const { return m_streamOutputBinds; } + Uint64 PatchParameters() const { return m_patchParameters; } + + protected: + // A backend change: the subclass lets go of what it held on the old one. + virtual void OnBackendChanged() {} + // The wire's user-index span (a draw whose indices rode in a stage segment). The record + // arm's port never hands one over - its indices are in the client's own buffer - so only + // the wire's subclass, which can check a span against its segments, answers true. + virtual Bool CheckUserIndices(const MG_Pipe::MGPDrawInfo&, const MG_Pipe::MGPDrawRange*, + const MG_Pipe::MGHostSpan&) { + return false; + } + + const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; + + // read_pixels' read, shared by the reply form and the pack-buffer form: the bound read + // framebuffer, NEUTRAL pack state (ID-49), `tight` bytes into m_readbackScratch. False + // when this server has no GL.ReadPixels - the caller owns what that means. + Bool ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight); + // get_texture_image's read, shared the same way: the whole level into `bytes`. + Bool ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector& bytes); + // P9: `tight` holds Src's pixels, tightly packed; land them in record.Dst at the + // record's layout through MGPipeApplyResourceSubData, one write per contiguous run. The + // layout was checked before the read (PipeApplier.cpp ReadbackToBufferLayoutFault). + Bool LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight, + Uint64 bytesPerPixel); + + MG_Backend::BackendObject* m_backend = nullptr; + Uint64 m_clears = 0; + Uint64 m_draws = 0; + Uint64 m_readbacks = 0; + Uint64 m_blits = 0; + Uint64 m_readbackBytes = 0; + Uint64 m_bufferReadbacks = 0; + Uint64 m_bufferReadbackWrites = 0; + Uint64 m_imageBinds = 0; + Uint64 m_dispatches = 0; + Uint64 m_memoryBarriers = 0; + Uint64 m_imageCopies = 0; + Uint64 m_storageBlockBindings = 0; + Uint64 m_streamOutputSpans = 0; + Uint64 m_streamOutputControls = 0; + Uint64 m_streamOutputBinds = 0; + Uint64 m_patchParameters = 0; + // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: + // ReadPixels writes into a caller buffer, so one staging vector per session sits + // between them. Grown, never shrunk, and never handed out past the call. + Vector m_readbackScratch; + // Server-declared LinkTerms.maxReplyBytes, copied from the attached server link. + Uint64 m_maxReplyBytes = 0; + // P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges + // per record (rule C: bounded by NumDraws, owned here, never handed out past the call), + // and the last-record witness above. + Vector m_multiCounts; + Vector m_multiFirsts; + Vector m_multiOffsets; + Vector m_multiBaseVertices; + LastDrawRecord m_lastDraw{}; + Uint64 m_drawRecords = 0; + }; + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 52d6551d..b7a73c23 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -180,6 +180,16 @@ namespace MobileGL::MG_Pipe { bool MGPipeRecordLatched() { return g_recordLatchedHook != nullptr && g_recordLatchedHook(); } + void MGPipeProtocolFatal(const char* what, const char* detail) { + MGPipeRecordFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, + detail != nullptr ? detail : ""); + } + + void MGPipeProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) { + MGPipeRecordFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", + what, static_cast(got), static_cast(expected)); + } + MGPipeRecordFailHook MGPipeRecordFailHookInstalled() { return g_recordFailHook; } MGPipeRecordLatchHook MGPipeRecordLatchHookInstalled() { return g_recordLatchHook; } MGPipeRecordLatchedHook MGPipeRecordLatchedHookInstalled() { return g_recordLatchedHook; } diff --git a/MobileGL/MG_Pipe/PipeSessionFail.h b/MobileGL/MG_Pipe/PipeSessionFail.h index dbde4ce1..23704f8b 100644 --- a/MobileGL/MG_Pipe/PipeSessionFail.h +++ b/MobileGL/MG_Pipe/PipeSessionFail.h @@ -41,6 +41,8 @@ #include +#include + namespace MobileGL::MG_Pipe { // THE FAMILIES THAT CROSS THIS BOUNDARY, AND ONLY THOSE. Not `MGFatalFamily` cast to an @@ -124,6 +126,10 @@ namespace MobileGL::MG_Pipe { MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...); // Whether the calling session already latched (false with no hook). bool MGPipeRecordLatched(); + // The record arm's two ProtocolCorruption lines, worded exactly as the wire codec's + // WireProtocolFatal / WireProtocolFatalAt (so every census and death matcher reads them alike). + [[noreturn]] void MGPipeProtocolFatal(const char* what, const char* detail); + [[noreturn]] void MGPipeProtocolFatalAt(const char* what, ::std::uint64_t got, ::std::uint64_t expected); #if MOBILEGL_BUILD_RECORD_ARM // ---- THE LATCH TWIN: A LOST GPU DEVICE ENDS ITS SESSION, NOT THE PROCESS ---------------- diff --git a/MobileGL/MG_Pipe/PipeVerbSink.h b/MobileGL/MG_Pipe/PipeVerbSink.h new file mode 100644 index 00000000..3ce9bb56 --- /dev/null +++ b/MobileGL/MG_Pipe/PipeVerbSink.h @@ -0,0 +1,256 @@ +// MobileGL - MobileGL/MG_Pipe/PipeVerbSink.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P13 W5: THE VERB SINK AND ITS REPLIES, WITHOUT THE WIRE. These were the wire codec's +// (MG_Remote/Wire/PipeWireCodec.h, which keeps their old names as aliases): the decoder hands each +// validated verb to a sink, and the sink answers through a reply sink. The record arm's in-process +// port does exactly the same in a library that has no wire, so the interfaces live here. + +#pragma once + +#include +#include + +namespace MobileGL::MG_Pipe { + + // One tail array. Two of the rows carry two (SetShaderBuffers, SetStreamOutputTargets), + // DrawVbo carries a conditional second one and P5e's SetProgramBindings carries three, + // which is why EncodeRecord's one-tail form could not stay the only one. + struct MGPipeVerbTail { + const void* Bytes = nullptr; + Uint64 Size = 0; + }; + + // Where a kReplySlot answer goes. Declared HERE and not in Server/ so the codec does not + // depend on the server session: the decoder's job ends at "produce the answer bytes". + // + // The slot is addressed seq % slots and the server writes the seq back into the slot + // header for self-check (table 0's slot header row). Status: 0 = OK, 1 = DECLINED, + // 2 = ERROR. DECLINED IS A REAL ANSWER, not a failure - it is how MapPersistent says + // nullptr (R-6) and how the four Bool acceptance entry points say false (R-5). + class MGPipeReplySink { + public: + virtual ~MGPipeReplySink() = default; + static constexpr Int32 kStatusOk = 0; + static constexpr Int32 kStatusDeclined = 1; + static constexpr Int32 kStatusError = 2; + virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0; + }; + + // ---- the verb sink: the five rows with no MGPipeApply* to delegate to --------------- + // + // The decoder owns NO semantics, so every arm ends in an existing MGPipeApply* free + // function - except five, and they are exactly contract §7's class B: Clear (57), Blit + // (56), ReadPixels (58), DrawVbo (59) and Present (67). Those are GLFunctionsTable VERBS. + // MG_Pipe has no applier for any of them (the 37 MGPipeApply* entry points are the object + // and state families), so for these five P5 writes the first consumer as well as the first + // producer - and the consumer is the SERVER'S backend call, which is v1's, not the codec's. + // + // So the codec does what it can prove and stops there: it bounds-checks, cross-checks the + // tail, resolves the segments and hands over a DECODED, VALIDATED argument list. With no + // sink installed those five arms return false ("this build does not implement it"), which + // is the same answer the other unimplemented rows give. + // + // SetShaderBuffers (38) and SetStreamOutputTargets (39) also have no applier entry point, + // and they deliberately get NO sink method: both are off P5's reduced path (BRIEF §4's + // exclusion list), so inventing a consumer for them would be building a semantics nobody + // can test this phase. Their arms validate both tails - which is the part a later phase + // must not have to re-derive - and return false. + struct MGPipeQueryResultReply { + Uint64 Value = 0; + Uint32 Produced = 0; + Uint32 Reserved = 0; + }; + static_assert(sizeof(MGPipeQueryResultReply) == 16); + + class MGPipeVerbSink { + public: + virtual ~MGPipeVerbSink() = default; + // P11 B2 (CONTRACT-P11.md B2): map_persistent in a session that runs T0. True = the sink + // handled it and `status` is the answer (OK: the client's AHardwareBuffer is now the + // store; DECLINED: the store runs T2); false = not a T0 session, and the codec gives + // today's constant decline. `seq` is the record's, which is what the store's Offer names. + virtual Bool OnMapPersistent(const MGPHandleOnly&, Uint64 /*seq*/, Int32& /*status*/) { + return false; + } + virtual Bool OnFenceCreate(const MGPHandleOnly&) { return false; } + virtual Bool OnFenceDestroy(const MGPHandleOnly&) { return false; } + virtual Bool OnFenceStatus(const MGPHandleOnly&, Uint32&) { return false; } + virtual Bool OnFenceWait(const MGPFenceWait&, Uint32&) { return false; } + virtual Bool OnFenceWaitServer(const MGPFenceWait&) { return false; } + virtual Bool OnQueryCreate(const MGPQueryDesc&) { return false; } + virtual Bool OnQueryBegin(const MGPQueryDesc&) { return false; } + virtual Bool OnQueryEnd(const MGPQueryDesc&) { return false; } + virtual Bool OnQueryCounter(const MGPQueryDesc&) { return false; } + virtual Bool OnQueryAvailable(const MGPHandleOnly&, Uint32&) { return false; } + virtual Bool OnQueryResult(const MGPQueryResultRequest&, MGPipeQueryResultReply&) { return false; } + virtual Bool OnQueryDestroy(const MGPHandleOnly&) { return false; } + virtual Bool OnQueryTimestamp(const MGPTimestampRequest&, Int64&) { return false; } + virtual Bool OnDeleteStreamOutput(const MGPStreamOutputBind&) { return false; } + virtual Bool OnClear(const MGPClear& clear) { + (void)clear; + return false; + } + virtual Bool OnBlit(const MGPBlit& blit) { + (void)blit; + return false; + } + virtual Bool OnPresent(const MGPPresent& present) { + (void)present; + return false; + } + // The pixels go back in the reply slot (contract table 1 row 23: the destination is + // ALWAYS SEG_REPLY in P5, which is why MGPReadbackInfo gains no Seg field), so the + // sink is handed the seq and the sink it must answer into. + virtual Bool OnReadPixels(const MGPReadbackInfo& info, Uint64 seq, MGPipeReplySink* replies) { + (void)info; + (void)seq; + (void)replies; + return false; + } + virtual Bool OnGetTextureImage(const MGPReadbackInfo&, Uint64, MGPipeReplySink*) { + return false; + } + // P9 (CONTRACT-P9.md §1): the same two reads with a pack buffer as the destination. No + // seq and no sink - nothing is answered; the rows land in `Dst` on this side. + virtual Bool OnReadPixelsToBuffer(const MGPReadbackToBuffer&) { return false; } + virtual Bool OnGetTextureImageToBuffer(const MGPReadbackToBuffer&) { return false; } + // `ranges` is info.NumDraws entries. `userIndices` is null unless the record set + // kDrawHasUserIndices; `indirect` is null unless it set kDrawIsIndirect (P5b d1, + // CONTRACT-P5B.md). The layout refuses a record that sets both, so at most one of the + // two is non-null. The span is VALIDATED (all four R-2 arms, the segment-range one + // included) and names a SEG_STAGE run the client staged; the sink resolves it through + // MGPipeHostBytes and never holds the pointer past its return (rule C). + virtual Bool OnDrawVbo(const MGPDrawInfo& info, const MGPDrawRange* ranges, + const MGHostSpan* userIndices, + const MGPDrawIndirect* indirect) { + (void)info; + (void)ranges; + (void)userIndices; + (void)indirect; + return false; + } + + // ---- P5b (MG_Remote/CONTRACT-P5B.md): the rows the four migration packages consume. + // + // Every one below is a GLFunctionsTable verb with NO MGPipeApply* entry point - the + // census's correction - so, exactly like the five above, the codec validates and hands + // over and the SERVER'S sink (Server/PipeApplier.cpp's ServerVerbSink) makes the + // backend call. The default bodies return false ("this build does not implement it"); + // ServerVerbSink's stubs die Fatal{UnmigratedVerb, ""} by name until the owning + // package lands the real body, so a client that flips a slot ahead of its server half + // aborts with the same line the census greps rather than rendering nothing. + // + // i1 OnLaunchGrid, OnMemoryBarrier, OnResourceCopyRegion, OnBindShaderImage, + // OnSetStorageBlockBinding + // t2 OnBeginStreamOutput, OnEndStreamOutput, OnPauseStreamOutput, + // OnResumeStreamOutput, OnBindStreamOutput, OnPatchParameter + // f1 OnGenerateMipmap, OnCopyFramebufferToTexture (and OnClear's non-Whole kinds) + // d1 OnDrawVbo's indirect tail and user-index span (above) + virtual Bool OnLaunchGrid(const MGPGridInfo& grid) { + (void)grid; + return false; + } + virtual Bool OnMemoryBarrier(const MGPMemoryBarrier& barrier) { + (void)barrier; + return false; + } + virtual Bool OnResourceCopyRegion(const MGPCopyRegion& copy) { + (void)copy; + return false; + } + virtual Bool OnBindShaderImage(const MGPImageBind& bind) { + (void)bind; + return false; + } + // `name` is the NUL-terminated block name the decoder copied out of the record's + // SEG_STAGE blob; valid for the call only. + virtual Bool OnSetStorageBlockBinding(const MGPStorageBlockBinding& binding, + const char* name) { + (void)binding; + (void)name; + return false; + } + virtual Bool OnBeginStreamOutput(const MGPStreamOutputBegin& begin) { + (void)begin; + return false; + } + virtual Bool OnEndStreamOutput(const MGPXfbAccounting& accounting) { + (void)accounting; + return false; + } + virtual Bool OnPauseStreamOutput(const MGPStreamOutputControl& control) { + (void)control; + return false; + } + virtual Bool OnResumeStreamOutput(const MGPStreamOutputControl& control) { + (void)control; + return false; + } + virtual Bool OnBindStreamOutput(const MGPStreamOutputBind& bind) { + (void)bind; + return false; + } + virtual Bool OnPatchParameter(const MGPPatchParameter& patch) { + (void)patch; + return false; + } + virtual Bool OnGenerateMipmap(const MGPMipPlan& plan) { + (void)plan; + return false; + } + virtual Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& copy) { + (void)copy; + return false; + } + + // ---- P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records, opcodes 77..78. + // + // Same hand-over shape as the P5b rows: the codec validates the record (a fixed-size + // POD, no blob, no tail) and the SERVER'S sink does the work - OnApplierReset runs the + // server's own MGPipeApplierReset() after asserting ContextSerial against the + // session's (§5.1: ASSERTED, never dispatched on, P5c has one context per session), + // and OnObjectDeath releases the kind's twin table by the handle the record carried + // (§5.2). The default bodies return false ("this build does not implement it"), so a + // unit decoder without a server declines by the same answer every other unimplemented + // row gives. + virtual Bool OnApplierReset(const MGPApplierReset& reset) { + (void)reset; + return false; + } + virtual Bool OnObjectDeath(const MGPHandleOnly& death) { + (void)death; + return false; + } + + // ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84. + // + // The applier_reset hand-over shape, one plane over: a fixed-size POD the bounds gate has + // already proved, no blob, no tail, no reply, so the codec validates nothing further and + // the SERVER'S sink does the work - the session's current context token becomes the + // record's. It is not an assertion like OnApplierReset's serial: the token is minted by + // the client's EGL layer and the server has no independent count of it, so an unknown + // token is the sink's own refusal to make and not a shape the codec can check. + virtual Bool OnBindContext(const MGPBindContext& bind) { + (void)bind; + return false; + } + + // ---- shared images, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). + // `seq` is the record's reply-slot id, which an Import's descriptor names on the aux + // socket. True fills `reply`; false is a DECLINED answer. + virtual Bool OnSharedImage(const MGPSharedImageOp& op, Uint64 seq, + MGPSharedImageReply& reply) { + (void)op; + (void)seq; + (void)reply; + return false; + } + }; + +} // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 22c43243..90bed7ec 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -102,34 +102,9 @@ namespace MobileGL::MG_Remote::Server { // 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(); - MG_Record::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(); + void ServerVerbSink::OnBackendChanged() { + ReleaseQueries(); + ReleaseFences(); } ServerVerbSink::FenceEntry& ServerVerbSink::FindFence(MG_Pipe::MGPipeHandle handle) { @@ -283,112 +258,6 @@ namespace MobileGL::MG_Remote::Server { #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(clear.BufferMask)); - break; - case kMGPClearKindColor: - switch (clear.ValueClass) { - case kMGPClearValueClassFloat: - if (gl.ClearBufferfv == nullptr) return false; - gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex, - reinterpret_cast(clear.ColorValue)); - break; - case kMGPClearValueClassInt: - if (gl.ClearBufferiv == nullptr) return false; - gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex, - reinterpret_cast(clear.ColorValue)); - break; - case kMGPClearValueClassUint: - if (gl.ClearBufferuiv == nullptr) return false; - gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex, - reinterpret_cast(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(blit.Mask), - static_cast(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). @@ -456,996 +325,6 @@ namespace MobileGL::MG_Remote::Server { 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(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 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(seq)); - } - const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel( - MG_Util::ConvertGLEnumToTextureInputFormat(static_cast(info.Format)), - MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast(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(std::numeric_limits::max()) || - info.Box.H > static_cast(std::numeric_limits::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(bytesPerPixel); - constexpr Uint64 kUint64Max = std::numeric_limits::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(std::numeric_limits::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(info.DstSize), - static_cast(tight), info.Box.W, info.Box.H, - static_cast(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(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 - // gPipeInputs->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(info.Box.W), - static_cast(info.Box.H), static_cast(info.Format), - static_cast(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& 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(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(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. - const MG_Pipe::MGPipeServerOriginatedWriteScope serverWrite; - 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(MG_Util::GetInputBytesPerPixel( - MG_Util::ConvertGLEnumToTextureInputFormat(static_cast(info.Format)), - MG_Util::ConvertGLEnumToTexturePixelDataType(static_cast(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::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(std::numeric_limits::max()) || - info.Box.H > static_cast(std::numeric_limits::max())) { - fault = "the read box or its format is invalid"; - } else if (static_cast(info.Box.W) > kUint64Max / info.Box.H || - static_cast(info.Box.W) * info.Box.H > kUint64Max / bpp) { - fault = "the tight extent overflows"; - } else { - tight = static_cast(info.Box.W) * info.Box.H * bpp; - if (m_maxReplyBytes == 0 || tight > m_maxReplyBytes || - tight > static_cast(std::numeric_limits::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(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(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 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(bytes.size()), - static_cast(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(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(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(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(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(static_cast(indirect->Offset)); - const auto drawCount = static_cast(indirect->DrawCount); - const auto stride = static_cast(indirect->Stride); - const Bool counted = !MG_Pipe::MGPipeHandleIsNull(indirect->ParameterBuffer); - const auto parameterOffset = static_cast(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(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(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(ranges[i].Start); - m_multiCounts[i] = static_cast(ranges[i].Count); - } - gl.MultiDrawArrays(mode, m_multiFirsts.data(), m_multiCounts.data(), - static_cast(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( - static_cast(ranges[i].Start) * info.IndexSize); - m_multiCounts[i] = static_cast(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(n), - m_multiBaseVertices.data()); - } - ++m_draws; - return true; - } - - // ---- one range ---------------------------------------------------------------------- - const MG_Pipe::MGPDrawRange& range = ranges[0]; - const auto count = static_cast(range.Count); - if (info.IndexSize == 0) { - if (!instanced) { - if (gl.DrawArrays == nullptr) return false; - gl.DrawArrays(mode, static_cast(range.Start), count); - } else if (baseInstance != 0) { - if (gl.DrawArraysInstancedBaseInstance == nullptr) return false; - gl.DrawArraysInstancedBaseInstance(mode, static_cast(range.Start), count, - instanceCount, baseInstance); - } else { - if (gl.DrawArraysInstanced == nullptr) return false; - gl.DrawArraysInstanced(mode, static_cast(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(static_cast(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("")`, 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(grid.IndirectOffset)); - } else { - if (gl.DispatchCompute == nullptr) return false; - gl.DispatchCompute(static_cast(grid.GridX), static_cast(grid.GridY), - static_cast(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(barrier.Bits)); - } else { - if (gl.MemoryBarrier == nullptr) return false; - gl.MemoryBarrier(static_cast(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(copy.SrcGlName), - static_cast(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(copy.SrcTarget), - static_cast(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y, - copy.SrcBox.Z, dst, static_cast(copy.DstTarget), - static_cast(copy.DstLevel), copy.DstX, copy.DstY, - copy.DstZ, static_cast(copy.SrcBox.W), - static_cast(copy.SrcBox.H), - static_cast(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(bind.Unit), static_cast(bind.GlName), - static_cast(bind.Level), - bind.Layered != 0 ? GL_TRUE : GL_FALSE, - static_cast(bind.Layer), - static_cast(bind.Access), - static_cast(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(binding.GlName), name, - static_cast(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(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(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(patch.Pname), static_cast(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) ------------------------------------------ // diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index cb788e04..a618c8e4 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -45,6 +45,7 @@ #include "../Transport/Ring.h" #include "../Wire/PipeWireCodec.h" +#include #include #include @@ -147,13 +148,12 @@ namespace MobileGL::MG_Remote::Server { // a split process is the client's emit table (table 3) - calling THAT here would re-emit // the record the server is in the middle of applying, which is an infinite loop that // renders nothing and looks like a hang. - class ServerVerbSink final : public Wire::WireVerbSink { + class ServerVerbSink final : public MG_Record::RecordVerbSink { public: - // The server's private backend. Null until ServerLoop::CreateBackend has run, and a - // verb that arrives before then declines by name rather than dereferencing. - void SetBackend(MG_Backend::BackendObject* backend); - MG_Backend::BackendObject* Backend() const { return m_backend; } - void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; } + // P13 W5: the record verbs (clear, blit, readbacks, draws, dispatch, copies, images, + // XFB, mipmaps) are MG_Record::RecordVerbSink's - the record arm applies them in a + // library without a transport too. What stays here is the session's: fences and + // queries, present, the T0 map, the control records and shared images. // P11 B2: T0's map_persistent - ServerSession::AdoptStoreT0 (it owns the aux socket). Bool OnMapPersistent(const MG_Pipe::MGPHandleOnly& handle, Uint64 seq, Int32& status) override; @@ -181,54 +181,7 @@ namespace MobileGL::MG_Remote::Server { Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) override; Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) override; void ReleaseQueries(); - Bool OnClear(const MG_Pipe::MGPClear& clear) override; - Bool OnBlit(const MG_Pipe::MGPBlit& blit) override; Bool OnPresent(const MG_Pipe::MGPPresent& present) override; - Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, - Wire::ReplySink* replies) override; - Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, - Wire::ReplySink* replies) override; - // P9 (CONTRACT-P9.md §1): the pack-buffer halves. Nothing is answered; the tight pixels - // land in record.Dst through the applier's buffer-write gate. - Bool OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override; - Bool OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override; - Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, - const MG_Pipe::MGHostSpan* userIndices, - const MG_Pipe::MGPDrawIndirect* indirect) override; - - // ---- P5b (MG_Remote/CONTRACT-P5B.md): one override per row a migration package owns. - // At the contract commit EVERY BODY BELOW IS A STUB that dies - // Fatal{UnmigratedVerb, ""} by the slot's own name - the same line the client's - // class-C table raises and the census greps - so a client flipped ahead of its server - // half aborts by name rather than rendering nothing, and the census on this head is - // unchanged (the client refuses first). The owning package replaces the body. - // - // i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion - // ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"), - // OnSetStorageBlockBinding ("ShaderStorageBlockBinding") - // t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput / - // OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies - // now: the backend call the contract names, the null-slot DECLINE, a tally. - // f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" / - // "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already) - // d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the - // instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots - // produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names) - Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override; - Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override; - Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override; - Bool OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) override; - Bool OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding, - const char* name) override; - Bool OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) override; - Bool OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) override; - Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; - Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; - Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override; - Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override; - Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override; - Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override; - Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override; // ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records ---------------- // @@ -272,78 +225,19 @@ namespace MobileGL::MG_Remote::Server { // assert the sequence rather than only the count. Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; } - // Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a - // split build is also what a scenario that ran entirely on the monolith path looks - // like (R-16: a probe may not arm against a stub). - Uint64 Clears() const { return m_clears; } - Uint64 Draws() const { return m_draws; } - Uint64 Readbacks() const { return m_readbacks; } - Uint64 Blits() const { return m_blits; } + // The session's per-verb tallies; the record verbs' are RecordVerbSink's. Uint64 Presents() const { return m_presents; } Uint64 LastPresentSerial() const { return m_lastPresentSerial; } - Uint64 ReadbackBytes() const { return m_readbackBytes; } - // P9: pack-buffer readbacks that landed in their buffer (both ops), and the buffer-write - // records they took. Readbacks() above counts only the reply-slot form. - Uint64 BufferReadbacks() const { return m_bufferReadbacks; } - Uint64 BufferReadbackWrites() const { return m_bufferReadbackWrites; } - // ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a - // split build "the scenario passed" is also what a scenario that never left the - // monolith path looks like - so the lane asserts the number that only this sink can - // move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry - // points and memory_barrier both barrier ones, and the sink is where they separate. - Uint64 ImageBinds() const { return m_imageBinds; } - Uint64 Dispatches() const { return m_dispatches; } - Uint64 MemoryBarriers() const { return m_memoryBarriers; } - Uint64 ImageCopies() const { return m_imageCopies; } - Uint64 StorageBlockBindings() const { return m_storageBlockBindings; } - // ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal - // the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from - // DstSize is exactly the heap overflow codex 1 found, one field over. - Uint64 ReadbackScratchBytes() const { return static_cast(m_readbackScratch.size()); } - - // ---- P5b d1: what the LAST draw_vbo record carried, as the sink saw it --------------- - // - // Recorded BEFORE the backend is consulted, so a process with no backend object (every - // unit case) can still assert the wire's fields rather than only that a draw "was - // declined": the record's head, its first range, its indirect block, and whether a - // user-index span rode with it and how many bytes it named. Nothing here outlives the - // call except these copies (rule C: the pointers the sink was handed are not kept). - struct LastDrawRecord { - MG_Pipe::MGPDrawInfo Info{}; - MG_Pipe::MGPDrawRange FirstRange{}; - MG_Pipe::MGPDrawIndirect Indirect{}; - Uint64 UserIndexBytes = 0; - Bool HadUserIndices = false; - Bool HadIndirect = false; - }; - const LastDrawRecord& LastDraw() const { return m_lastDraw; } - // draw_vbo records seen, applied or declined; Draws() above counts only the applied. - Uint64 DrawRecords() const { return m_drawRecords; } - // P5b t2's tallies, for the same reason the five above exist (R-16): under split "the - // scenario passed" is also what a scenario that ran entirely on the monolith path looks - // like, so a lane that wants to say the XFB spans CROSSED has to read a counter the - // server moved. Spans counts Begin and End together - they are one span and a lane that - // saw only one of them has a bug the two-counter version would have hidden behind a - // sum; controls counts Pause and Resume; binds and patch parameters count their own. - Uint64 StreamOutputSpans() const { return m_streamOutputSpans; } - Uint64 StreamOutputControls() const { return m_streamOutputControls; } - Uint64 StreamOutputBinds() const { return m_streamOutputBinds; } - Uint64 PatchParameters() const { return m_patchParameters; } private: - const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; - - // read_pixels' read, shared by the reply form and the pack-buffer form: the bound read - // framebuffer, NEUTRAL pack state (ID-49), `tight` bytes into m_readbackScratch. False - // when this server has no GL.ReadPixels - the caller owns what that means. - Bool ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight); - // get_texture_image's read, shared the same way: the whole level into `bytes`. - Bool ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector& bytes); - // P9: `tight` holds Src's pixels, tightly packed; land them in record.Dst at the - // record's layout through MGPipeApplyResourceSubData, one write per contiguous run. The - // layout was checked before the read (PipeApplier.cpp ReadbackToBufferLayoutFault). - Bool LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight, - Uint64 bytesPerPixel); + // A backend change ends every fence and query this sink holds (they are the old + // backend's natives). + void OnBackendChanged() override; + // The wire's user-index span is checked against this session's segments. + Bool CheckUserIndices(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, + const MG_Pipe::MGHostSpan& span) override { + return Wire::CheckDrawUserIndices(info, ranges, span); + } struct FenceEntry { Uint32 Gen = 0; @@ -368,25 +262,8 @@ namespace MobileGL::MG_Remote::Server { void EndNativeQuery(QueryEntry& entry); Bool QueryGpuComplete(QueryEntry& entry, Bool wait); UnorderedMap m_queries; - MG_Backend::BackendObject* m_backend = nullptr; - Uint64 m_clears = 0; - Uint64 m_draws = 0; - Uint64 m_readbacks = 0; - Uint64 m_blits = 0; Uint64 m_presents = 0; Uint64 m_lastPresentSerial = 0; - Uint64 m_readbackBytes = 0; - Uint64 m_bufferReadbacks = 0; - Uint64 m_bufferReadbackWrites = 0; - Uint64 m_imageBinds = 0; - Uint64 m_dispatches = 0; - Uint64 m_memoryBarriers = 0; - Uint64 m_imageCopies = 0; - Uint64 m_storageBlockBindings = 0; - Uint64 m_streamOutputSpans = 0; - Uint64 m_streamOutputControls = 0; - Uint64 m_streamOutputBinds = 0; - Uint64 m_patchParameters = 0; // P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record // and the count of accepted resets: the two are one number because the serial is the // session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never @@ -400,21 +277,6 @@ namespace MobileGL::MG_Remote::Server { // the object the control plane's CreateContext/DestroyContext also own - one table, one // owner, and the sink is only the ring's door into it. Uint64 m_bindContexts = 0; - // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: - // ReadPixels writes into a caller buffer, so one staging vector per session sits - // between them. Grown, never shrunk, and never handed out past the call. - Vector m_readbackScratch; - // Server-declared LinkTerms.maxReplyBytes, copied from the attached server link. - Uint64 m_maxReplyBytes = 0; - // P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges - // per record (rule C: bounded by NumDraws, owned here, never handed out past the call), - // and the last-record witness above. - Vector m_multiCounts; - Vector m_multiFirsts; - Vector m_multiOffsets; - Vector m_multiBaseVertices; - LastDrawRecord m_lastDraw{}; - Uint64 m_drawRecords = 0; // The newest native fence (shared_image NativeFence) this session handed out, kept so a // later fence with nothing left to export still answers with a descriptor: by then this // one has signaled (the backend waited the work out), and a signaled sync_file stays so. diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 7cff8439..7fa29ae5 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -42,6 +42,7 @@ #include #include +#include #include "../Transport/Ring.h" #include "../Transport/Doorbell.h" @@ -235,13 +236,8 @@ namespace MobileGL::MG_Remote::Wire { // The catalogue's own spelling of an opcode, for a Fatal line. Out of range is "". const char* WireOpName(MG_Pipe::MGPWireOp op); - // One tail array. Two of the rows carry two (SetShaderBuffers, SetStreamOutputTargets), - // DrawVbo carries a conditional second one and P5e's SetProgramBindings carries three, - // which is why EncodeRecord's one-tail form could not stay the only one. - struct WireTail { - const void* Bytes = nullptr; - Uint64 Size = 0; - }; + // P13 W5: the tail is MG_Pipe's (PipeVerbSink.h) - the record arm's in-process port speaks it too. + using WireTail = ::MobileGL::MG_Pipe::MGPipeVerbTail; // ---- encoder ----------------------------------------------------------------------- // @@ -473,232 +469,11 @@ namespace MobileGL::MG_Remote::Wire { // ---- decoder ----------------------------------------------------------------------- - // Where a kReplySlot answer goes. Declared HERE and not in Server/ so the codec does not - // depend on the server session: the decoder's job ends at "produce the answer bytes". - // - // The slot is addressed seq % slots and the server writes the seq back into the slot - // header for self-check (table 0's slot header row). Status: 0 = OK, 1 = DECLINED, - // 2 = ERROR. DECLINED IS A REAL ANSWER, not a failure - it is how MapPersistent says - // nullptr (R-6) and how the four Bool acceptance entry points say false (R-5). - class ReplySink { - public: - virtual ~ReplySink() = default; - static constexpr Int32 kStatusOk = 0; - static constexpr Int32 kStatusDeclined = 1; - static constexpr Int32 kStatusError = 2; - virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0; - }; - - // ---- the verb sink: the five rows with no MGPipeApply* to delegate to --------------- - // - // The decoder owns NO semantics, so every arm ends in an existing MGPipeApply* free - // function - except five, and they are exactly contract §7's class B: Clear (57), Blit - // (56), ReadPixels (58), DrawVbo (59) and Present (67). Those are GLFunctionsTable VERBS. - // MG_Pipe has no applier for any of them (the 37 MGPipeApply* entry points are the object - // and state families), so for these five P5 writes the first consumer as well as the first - // producer - and the consumer is the SERVER'S backend call, which is v1's, not the codec's. - // - // So the codec does what it can prove and stops there: it bounds-checks, cross-checks the - // tail, resolves the segments and hands over a DECODED, VALIDATED argument list. With no - // sink installed those five arms return false ("this build does not implement it"), which - // is the same answer the other unimplemented rows give. - // - // SetShaderBuffers (38) and SetStreamOutputTargets (39) also have no applier entry point, - // and they deliberately get NO sink method: both are off P5's reduced path (BRIEF §4's - // exclusion list), so inventing a consumer for them would be building a semantics nobody - // can test this phase. Their arms validate both tails - which is the part a later phase - // must not have to re-derive - and return false. - struct QueryResultReply { - Uint64 Value = 0; - Uint32 Produced = 0; - Uint32 Reserved = 0; - }; - static_assert(sizeof(QueryResultReply) == 16); - - class WireVerbSink { - public: - virtual ~WireVerbSink() = default; - // P11 B2 (CONTRACT-P11.md B2): map_persistent in a session that runs T0. True = the sink - // handled it and `status` is the answer (OK: the client's AHardwareBuffer is now the - // store; DECLINED: the store runs T2); false = not a T0 session, and the codec gives - // today's constant decline. `seq` is the record's, which is what the store's Offer names. - virtual Bool OnMapPersistent(const MG_Pipe::MGPHandleOnly&, Uint64 /*seq*/, Int32& /*status*/) { - return false; - } - virtual Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) { return false; } - virtual Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) { return false; } - virtual Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } - virtual Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) { return false; } - virtual Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) { return false; } - virtual Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) { return false; } - virtual Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) { return false; } - virtual Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) { return false; } - virtual Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) { return false; } - virtual Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } - virtual Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, QueryResultReply&) { return false; } - virtual Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) { return false; } - virtual Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) { return false; } - virtual Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind&) { return false; } - virtual Bool OnClear(const MG_Pipe::MGPClear& clear) { - (void)clear; - return false; - } - virtual Bool OnBlit(const MG_Pipe::MGPBlit& blit) { - (void)blit; - return false; - } - virtual Bool OnPresent(const MG_Pipe::MGPPresent& present) { - (void)present; - return false; - } - // The pixels go back in the reply slot (contract table 1 row 23: the destination is - // ALWAYS SEG_REPLY in P5, which is why MGPReadbackInfo gains no Seg field), so the - // sink is handed the seq and the sink it must answer into. - virtual Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, ReplySink* replies) { - (void)info; - (void)seq; - (void)replies; - return false; - } - virtual Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo&, Uint64, ReplySink*) { - return false; - } - // P9 (CONTRACT-P9.md §1): the same two reads with a pack buffer as the destination. No - // seq and no sink - nothing is answered; the rows land in `Dst` on this side. - virtual Bool OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer&) { return false; } - virtual Bool OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer&) { return false; } - // `ranges` is info.NumDraws entries. `userIndices` is null unless the record set - // kDrawHasUserIndices; `indirect` is null unless it set kDrawIsIndirect (P5b d1, - // CONTRACT-P5B.md). The layout refuses a record that sets both, so at most one of the - // two is non-null. The span is VALIDATED (all four R-2 arms, the segment-range one - // included) and names a SEG_STAGE run the client staged; the sink resolves it through - // MG_Pipe::MGPipeHostBytes and never holds the pointer past its return (rule C). - virtual Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, - const MG_Pipe::MGHostSpan* userIndices, - const MG_Pipe::MGPDrawIndirect* indirect) { - (void)info; - (void)ranges; - (void)userIndices; - (void)indirect; - return false; - } - - // ---- P5b (MG_Remote/CONTRACT-P5B.md): the rows the four migration packages consume. - // - // Every one below is a GLFunctionsTable verb with NO MGPipeApply* entry point - the - // census's correction - so, exactly like the five above, the codec validates and hands - // over and the SERVER'S sink (Server/PipeApplier.cpp's ServerVerbSink) makes the - // backend call. The default bodies return false ("this build does not implement it"); - // ServerVerbSink's stubs die Fatal{UnmigratedVerb, ""} by name until the owning - // package lands the real body, so a client that flips a slot ahead of its server half - // aborts with the same line the census greps rather than rendering nothing. - // - // i1 OnLaunchGrid, OnMemoryBarrier, OnResourceCopyRegion, OnBindShaderImage, - // OnSetStorageBlockBinding - // t2 OnBeginStreamOutput, OnEndStreamOutput, OnPauseStreamOutput, - // OnResumeStreamOutput, OnBindStreamOutput, OnPatchParameter - // f1 OnGenerateMipmap, OnCopyFramebufferToTexture (and OnClear's non-Whole kinds) - // d1 OnDrawVbo's indirect tail and user-index span (above) - virtual Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) { - (void)grid; - return false; - } - virtual Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) { - (void)barrier; - return false; - } - virtual Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) { - (void)copy; - return false; - } - virtual Bool OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) { - (void)bind; - return false; - } - // `name` is the NUL-terminated block name the decoder copied out of the record's - // SEG_STAGE blob; valid for the call only. - virtual Bool OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding, - const char* name) { - (void)binding; - (void)name; - return false; - } - virtual Bool OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) { - (void)begin; - return false; - } - virtual Bool OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) { - (void)accounting; - return false; - } - virtual Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) { - (void)control; - return false; - } - virtual Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) { - (void)control; - return false; - } - virtual Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) { - (void)bind; - return false; - } - virtual Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) { - (void)patch; - return false; - } - virtual Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) { - (void)plan; - return false; - } - virtual Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) { - (void)copy; - return false; - } - - // ---- P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records, opcodes 77..78. - // - // Same hand-over shape as the P5b rows: the codec validates the record (a fixed-size - // POD, no blob, no tail) and the SERVER'S sink does the work - OnApplierReset runs the - // server's own MGPipeApplierReset() after asserting ContextSerial against the - // session's (§5.1: ASSERTED, never dispatched on, P5c has one context per session), - // and OnObjectDeath releases the kind's twin table by the handle the record carried - // (§5.2). The default bodies return false ("this build does not implement it"), so a - // unit decoder without a server declines by the same answer every other unimplemented - // row gives. - virtual Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) { - (void)reset; - return false; - } - virtual Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) { - (void)death; - return false; - } - - // ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84. - // - // The applier_reset hand-over shape, one plane over: a fixed-size POD the bounds gate has - // already proved, no blob, no tail, no reply, so the codec validates nothing further and - // the SERVER'S sink does the work - the session's current context token becomes the - // record's. It is not an assertion like OnApplierReset's serial: the token is minted by - // the client's EGL layer and the server has no independent count of it, so an unknown - // token is the sink's own refusal to make and not a shape the codec can check. - virtual Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) { - (void)bind; - return false; - } - - // ---- shared images, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). - // `seq` is the record's reply-slot id, which an Import's descriptor names on the aux - // socket. True fills `reply`; false is a DECLINED answer. - virtual Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq, - MG_Pipe::MGPSharedImageReply& reply) { - (void)op; - (void)seq; - (void)reply; - return false; - } - }; + // P13 W5: the reply sink and the verb sink are MG_Pipe's (PipeVerbSink.h): the record arm + // applies the same verbs in a library without a transport, through the same interface. + using ReplySink = ::MobileGL::MG_Pipe::MGPipeReplySink; + using QueryResultReply = ::MobileGL::MG_Pipe::MGPipeQueryResultReply; + using WireVerbSink = ::MobileGL::MG_Pipe::MGPipeVerbSink; // Not thread safe: one decoder on the apply thread, by construction. class PipeWireDecoder {