diff --git a/CMakeLists.txt b/CMakeLists.txt index f97d3fe3..46736182 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -697,6 +697,10 @@ 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 + # P13 W5: the verb emitters and the monolith verb port, and the client GPU-write set + # they mark before a draw or a dispatch (MG_Record; EmitTables.cpp resolves the session). + MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp + MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp # The record verbs' consumer (ServerVerbSink under a transport is this plus the session's). MobileGL/MG_Backend/Record/RecordVerbSink.cpp ) diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index a2fa8ff3..72195d60 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -24,8 +24,10 @@ #if MOBILEGL_PIPE_VERIFY #include // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl #endif +#if MOBILEGL_BUILD_RECORD_ARM +#include +#endif #if MOBILEGL_BUILD_DISAGGREGATED -#include #include #endif @@ -183,13 +185,13 @@ namespace MobileGL { #endif { MG_Backend::Init(); -#if MOBILEGL_BUILD_DISAGGREGATED +#if MOBILEGL_BUILD_RECORD_ARM // P13 W4: monolith's record arm reads the verb's own state, which only a verb sink - // writes; the port routes the ported verbs through one (MG_Remote/Client/EmitTables.cpp). + // writes; the port routes the ported verbs through one (MG_Impl/Pipe/Verb/VerbPort.cpp). // Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the // port is client code, and the server image must not need it to link. if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) { - MG_Remote::Client::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable); + MG_Record::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable); } #endif } diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index b6e92b07..c9f03aa5 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -12,8 +12,8 @@ #include #include #include -#if MOBILEGL_BUILD_DISAGGREGATED -#include +#if MOBILEGL_BUILD_RECORD_ARM +#include #endif // CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the // client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test @@ -1386,11 +1386,11 @@ namespace MobileGL::MG_Impl::GLImpl { MGP_FILL(EndTransformFeedback); endXfb(); } -#if MOBILEGL_BUILD_DISAGGREGATED +#if MOBILEGL_BUILD_RECORD_ARM // P5 (b1), the second producer the client-side GPU-write set ADDS, and it has to be // taken HERE - before GLContext::EndTransformFeedback clears the live bindings, since // a mark taken after it marks nothing. - MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets(); + MG_Record::MarkEndTransformFeedbackCaptureTargets(); #endif MG_State::pGLContext->EndTransformFeedback(); // Captured results must be visible to MapBuffer/GetBufferSubData after diff --git a/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp new file mode 100644 index 00000000..97e55644 --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp @@ -0,0 +1,182 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.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 client GPU-write set (GpuWriteSet.h), moved from MG_Remote/Client/GpuWritePending.cpp. + +#include "GpuWriteSet.h" + +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Record { + + using MG_State::GLState::BufferObject; + using MobileGL::BufferTarget; + using MG_State::GLState::ImageTextureBinding; + + namespace { + // Per-row tallies. Diagnostics, and the only thing a unit case can assert on: an + // over-approximating set has no observable difference when a row fires too OFTEN, so + // "did this row fire at all, for this buffer" has to be readable directly. + Array(GpuWriteProducer::Count)> g_producerMarks{}; + + // The transform-feedback rows, shared by the draw walk (row 3) and by + // glEndTransformFeedback (row 5). Both mark the SAME set - the capture targets of the + // capture program - and the split exists only so the two can be counted apart. + void MarkTransformFeedbackTargets(GpuWriteProducer producer) { + auto& context = MG_State::pGLContext; + if (!context) return; + if (!context->IsTransformFeedbackActive()) return; + const auto& program = context->GetTransformFeedbackProgram(); + if (program == nullptr) return; + const SizeT declared = program->GetTransformFeedbackBufferCount(); + const SizeT count = + declared < MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS + ? declared + : static_cast(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS); + for (SizeT i = 0; i < count; ++i) { + const auto& point = + context->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); + MarkBufferForProducer(point.GetBoundObject(), producer); + } + } + + void MarkShaderStorageBindings() { + auto& context = MG_State::pGLContext; + if (!context) return; + // The TOUCHED count, exactly as the backend twin uses it + // (DirectGLES.cpp:559-560): the binding-point array is 84 entries wide and + // walking all of them on every draw is what the high-water mark exists to avoid. + const SizeT points = context->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage); + for (SizeT i = 0; i < points; ++i) { + const auto& point = context->GetBufferBindingPoint(BufferTarget::ShaderStorage, static_cast(i)); + MarkBufferForProducer(point.GetBoundObject(), GpuWriteProducer::ShaderStorageBinding); + } + } + + void MarkAtomicCounterBindings() { + auto& context = MG_State::pGLContext; + if (!context) return; + // WIDER THAN ITS BACKEND TWIN, ON PURPOSE AND IN THE SAFE DIRECTION. + // SyncAtomicCounterBuffers (DirectGLES.cpp:578-583) walks the GL bindings the + // TRANSPILED program declared, which is a subset of what is bound; the client has + // the bindings but not that per-program list at this point, so it marks every + // touched atomic-counter point. Over-approximating costs a readback the narrowing + // channel then removes. Under-approximating reads a stale counter and says + // nothing, which is the failure every atomic-counter conformance case is. + const SizeT points = context->GetTouchedBufferBindingPointCount(BufferTarget::AtomicCounter); + for (SizeT i = 0; i < points; ++i) { + const auto& point = context->GetBufferBindingPoint(BufferTarget::AtomicCounter, static_cast(i)); + MarkBufferForProducer(point.GetBoundObject(), GpuWriteProducer::AtomicCounterBinding); + } + } + + void MarkWritableImageBufferTextures() { + auto& context = MG_State::pGLContext; + if (!context) return; + // The backend keeps a bitset of writable image-buffer units + // (DirectGLES.cpp:2350-2352, maintained from its own SyncImageTextureBinding) and + // the client has no equivalent, so it sweeps. The sweep is NOT bounded by a device + // limit read: MaxImageUnits would be a backend read, and a stale or absent backend + // object would silently shorten the walk - which is the one direction this set may + // not fail in. + // + // IT IS BOUNDED BY THE FRONTEND's OWN IMAGE-UNIT HIGH-WATER MARK (P5d round 3, + // package C). The paragraph that stood here said the narrowing "belongs with P8's + // binding-walk migration"; the 2026-09-17 inproc profile moved it forward - + // MarkWritableImageBufferTextures was 2.15% self / 3.59% inclusive of the GL thread + // (GetImageTextureBinding 1.37 of it) at ~852 draws/frame, in a workload that never + // binds an image at all, purely because the walk was MAX_TEXTURE_IMAGE_UNITS (192) + // units wide unconditionally - 192 GetImageTextureBinding reads per draw for a + // context that has never bound one. + // + // AND THE MARK IS SAFE IN THE ONLY DIRECTION THAT MATTERS. It is written by + // glBindImageTexture itself (GL_Texture.cpp, TextureState::NoteImageUnitTouched) and + // it only ever grows - an unbind, a delete-unbind, a context that stops using an + // image unit all leave it where it was - so `unit <= mark` can only be WIDER than + // the set of units that hold a binding, never narrower. -1 means no image unit has + // ever been bound in this context, and then this is one compare. + const Int highest = context->GetMaxTouchedImageUnit(); + for (Int unit = 0; unit <= highest && unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; + ++unit) { + const auto& binding = context->GetImageTextureBinding(unit); + if (!ImageUnitIsAWritableBufferTexture(binding)) continue; + auto* textureBuffer = static_cast(binding.Texture.get()); + MarkBufferForProducer(textureBuffer->GetBufferBindingSlot().GetBoundObject(), + GpuWriteProducer::WritableImageBufferTexture); + } + } + } // namespace + + Bool GpuWriteSetIsClientSide() { + // P13 W4a: wherever the backend reads records - every wire, and monolith's record arm - + // the client owns the conservative set; the backend marks only its own twins. + return MG_Config::DataArmIsRecord(); + } + + Bool ImageUnitIsAWritableBufferTexture(const ImageTextureBinding& binding) { + // Verbatim from IsWritableImageBufferTexture (DirectGLES.cpp:2354-2357). All three + // terms are client state; none of them is a driver question. + return binding.Texture != nullptr && binding.Access != GL_READ_ONLY && + binding.Texture->GetStorageType() == TextureStorageType::Buffer; + } + + void MarkBufferForProducer(const SharedPtr& buffer, GpuWriteProducer producer) { + if (!GpuWriteSetIsClientSide()) return; + if (buffer == nullptr) return; + if (producer >= GpuWriteProducer::Count) return; + buffer->MarkGpuWritten(); + ++g_producerMarks[static_cast(producer)]; + } + + void MarkGpuWritesForDraw() { + if (!GpuWriteSetIsClientSide()) return; + MarkShaderStorageBindings(); + MarkAtomicCounterBindings(); + MarkWritableImageBufferTextures(); + MarkTransformFeedbackTargets(GpuWriteProducer::TransformFeedbackCapture); + } + + void MarkGpuWritesForDispatch() { + if (!GpuWriteSetIsClientSide()) return; + MarkShaderStorageBindings(); + MarkAtomicCounterBindings(); + MarkWritableImageBufferTextures(); + } + + void MarkReadPixelsPackBuffer() { + if (!GpuWriteSetIsClientSide()) return; + auto& context = MG_State::pGLContext; + if (!context) return; + MarkBufferForProducer(context->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(), + GpuWriteProducer::ReadPixelsPackBuffer); + } + + void MarkEndTransformFeedbackCaptureTargets() { + if (!GpuWriteSetIsClientSide()) return; + MarkTransformFeedbackTargets(GpuWriteProducer::EndTransformFeedbackCapture); + } + + Uint64 ProducerMarkCount(GpuWriteProducer producer) { + if (producer >= GpuWriteProducer::Count) return 0; + return g_producerMarks[static_cast(producer)]; + } + + void ResetProducerMarkCountsForTest() { + g_producerMarks.fill(0); + } + + Bool BufferWritebackIsReachable(const BufferObject& buffer) { + if (buffer.GetSize() == 0) return false; + return MG_Pipe::MGPipeResourceSubsystemEnabled(); + } + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.h b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.h new file mode 100644 index 00000000..c7f7661f --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.h @@ -0,0 +1,147 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.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 + +// THE CLIENT-SIDE CONSERVATIVE GPU-WRITE SET (ARCHITECTURE.md:575 names this component; +// CONTRACT-P5.md section 3, table 2's first set). Owner: package b1. +// +// WHY IT HAS TO MOVE SIDES. `BufferObject::SyncGpuWrites` (BufferObject.cpp:369) runs +// synchronously, on the application's thread, the moment the application calls glMapBuffer or +// glGetBufferSubData. It has to answer "did the GPU write this buffer since I last read it?" +// - and today all six producers of that answer are BACKEND-side (DirectGLES.cpp:570, :618, +// :2603, UniformManager.cpp:1075, :1231, VulkanRenderer.cpp:11618), i.e. on the server's side +// of a split. Asking the server is a round trip the design forbids in steady state, so the +// client must build the set itself, from state it already owns: the SSBO and atomic-counter +// binding points, the image units whose Access is not GL_READ_ONLY, and the transform-feedback +// capture targets. +// +// CONSERVATIVE MEANS OVER-APPROXIMATE, AND THAT IS THE WHOLE DESIGN. The reverse channel's +// OnGpuWritten is a NARROWING channel (ResourceTracker.h:577-581): the client marks +// everything a shader COULD have written and the server only ever removes entries. So a row +// here that fires too often costs a readback; a row that fires too rarely reads a stale shadow +// and is silent. Every row below therefore mirrors its backend twin exactly, including the two +// places the twin is deliberately NARROW - a GL_READ_ONLY image binding is left alone, and the +// image walk runs from draw preparation rather than from glBindImageTexture's eager sync. +// +// NO NARROWING IN P5. ResourceTracker.h:587-592's `rangeCount == 1` assertion STAYS. Zero +// ranges will mean "a fully narrowed set - nothing is dirty" at P8/P9, and a package that +// reads ResourceTracker.h:577-581 alone will think it owns that already. It does not. +// +// THE TWO NEW PRODUCERS. Rows 4 and 5 have no backend twin: they are behaviour P5 ADDS +// (ARCHITECTURE.md:508), and both are strictly better than what monolith does. +// * glReadPixels into a pack PBO becomes fire-and-forget plus a client-side mark. Monolith +// maps the PBO and copies it into the shadow inside the call (DirectGLES.cpp:10983-10993), +// which is an unconditional stall on every glReadPixels whether or not anyone reads the +// shadow; marking instead defers the cost to the first read that actually wants it. +// * glEndTransformFeedback drops its unbounded ClientWaitSync (GL_Drawing.cpp:1367, timeout +// ~0ull) and marks the capture targets, for the same reason: the wait exists only so that +// a later MapBuffer sees real results, which is precisely what the flag is for. +// +// WHAT NO ROW COVERS, WRITTEN DOWN SO THE NEXT READER DOES NOT HAVE TO ASK. The set is built +// from the APPLICATION's bindings, so it says nothing about a backend's own scratch buffers - +// Espryt's converted-vertex-stream and primitive-restart substitution buffers, Magma's UBO +// ring. None of those has a MarkGpuWritten today either, so the client set is no NARROWER than +// monolith's and this is not a regression; it is a standing hole in both, and it stays one +// until the phase that migrates the backend's own allocations. +// +// GATED ON THE TRANSPORT, like everything else in this package: on the monolith path the six +// backend sites still run and a second marker would be new behaviour (D-J), and rows 4 and 5 +// would remove a stall monolith is entitled to keep. + +// P13 W5: moved from MG_Remote/Client/GpuWritePending.h - the record arm owns this set in a +// library without a transport too. The session's waits stay there. + +#pragma once +#include + +#include + +namespace MobileGL::MG_State::GLState { + class BufferObject; + struct ImageTextureBinding; +} // namespace MobileGL::MG_State::GLState + +namespace MobileGL::MG_Record { + + // ONE ROW PER PRODUCER, and the enum is the inventory: six that mirror a backend + // MarkGpuWritten site one-for-one, two that P5 adds. Each has exactly one unit case, and + // the per-row counters below are what those cases assert on - a row that stops firing is + // otherwise invisible, because an over-approximating set fails SILENTLY in the direction + // that matters. + enum class GpuWriteProducer : Uint8 { + // DirectGLES.cpp:570 (MarkShaderStorageBuffersGpuWritten) and + // UniformManager.cpp:1231 (ResolveStorageBufferDescriptor): every SSBO binding point, + // unconditional, once the points are bound and the draw or dispatch is going out. + ShaderStorageBinding = 0, + // DirectGLES.cpp:618 (SyncAtomicCounterBuffers): every bound atomic counter. The + // point of a counter is that the shader increments it and every conformance case + // reads the increment back with glMapBufferRange or glGetBufferSubData. + AtomicCounterBinding, + // DirectGLES.cpp:2603 (MarkWritableImageBufferTexturesGpuWritten) and + // UniformManager.cpp:1075 (ResolveStorageTexelBufferDescriptor): a buffer texture on + // an image unit, ONLY when Access != GL_READ_ONLY. Marking a read-only binding would + // make the next map wait on - and then re-read - a dispatch that could not have + // changed a byte of it. + WritableImageBufferTexture, + // VulkanRenderer.cpp:11618 (BeginXfbCaptureForDraw): the capture targets, because + // "the capture is a GPU write like any shader's". + TransformFeedbackCapture, + // P5, new: glReadPixels into a bound GL_PIXEL_PACK_BUFFER. + ReadPixelsPackBuffer, + // P5, new: glEndTransformFeedback, in place of the unbounded fence wait. + EndTransformFeedbackCapture, + Count + }; + + // The backend reads records (MG_Config::DataArmIsRecord). False means every entry point + // below is a no-op and the backend sites are the only producers. + Bool GpuWriteSetIsClientSide(); + + // ---- the walks ------------------------------------------------------------------- + // + // Called from the client's own draw / dispatch emission point, BEFORE the verb record + // goes out, for the same ordering reason the persistent-map push has: the set must + // describe the work the record is about to start. + + // Rows 0, 1, 2 and 3. + void MarkGpuWritesForDraw(); + // Rows 0, 1 and 2. A dispatch has no transform feedback. + void MarkGpuWritesForDispatch(); + + // ---- the two new producers ------------------------------------------------------- + + // Row 4. Marks whatever is bound to GL_PIXEL_PACK_BUFFER, or nothing when the read goes + // to client memory - which is the case the backend's map-and-copy never had to consider, + // because it only ran when a PBO was bound in the first place. + void MarkReadPixelsPackBuffer(); + // Row 5. Must be called while the capture state is still ACTIVE: GLContext's + // EndTransformFeedback clears the live bindings, so a mark taken after it marks nothing. + void MarkEndTransformFeedbackCaptureTargets(); + + // ---- the row predicates, exposed so a unit case can drive one row at a time ------- + + // Row 2's discriminator, verbatim from DirectGLES.cpp:2354-2357. It is a function rather + // than three inline conditions at the call site because it is the one row whose backend + // twin is narrow on purpose, and a client that re-derived it slightly wider would mark + // read-only image bindings with nothing able to see that it had. + Bool ImageUnitIsAWritableBufferTexture(const MG_State::GLState::ImageTextureBinding& binding); + + // The one place a row actually marks. Null and duplicate marks are absorbed here so the + // walks stay readable, and the per-row counter moves only when a buffer really was + // marked. + void MarkBufferForProducer(const SharedPtr& buffer, + GpuWriteProducer producer); + + Uint64 ProducerMarkCount(GpuWriteProducer producer); + void ResetProducerMarkCountsForTest(); + + // Is there a readback route at all? A buffer with no size, or one whose backend registered + // no resource ops, can never catch up, and SyncGpuWrites must clear rather than block for + // ever. It is the ONE case monolith's unconditional clear covers that a writeback cannot. + Bool BufferWritebackIsReachable(const MG_State::GLState::BufferObject& buffer); + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc b/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc similarity index 97% rename from MobileGL/MG_Remote/Client/TextureReadbackEmit.inc rename to MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc index ad6e5067..45a6a9b0 100644 --- a/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc +++ b/MobileGL/MG_Impl/Pipe/Verb/TextureReadbackEmit.inc @@ -1,4 +1,4 @@ -// Included in EmitTables.cpp's client-only anonymous namespace. +// Included in VerbPort.cpp's anonymous namespace (P13 W5: moved from MG_Remote/Client). // The server returns owned tight pixels. Only the client interprets PACK state // and application pointers. A pack-buffer destination (P9) is answered by nothing: the // client sends the layout it computed and the server lands the rows in the buffer; the @@ -132,11 +132,11 @@ } Vector bytes(static_cast(tight)); const Uint64 capacity = session.MaxReplyBytes(); - if (!capacity) Wire::WireProtocolFatal("GetTextureImage.reply", "zero reply capacity"); + if (!capacity) MG_Pipe::MGPipeProtocolFatal("GetTextureImage.reply", "zero reply capacity"); for (Uint64 offset = 0; offset < tight; ) { info.DstOffset = offset; info.DstSize = std::min(tight - offset, capacity); - Int32 status = Wire::ReplySink::kStatusError; + Int32 status = MG_Pipe::MGPipeReplySink::kStatusError; Uint64 received = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::GetTextureImage, &info, sizeof(info), nullptr, 0, bytes.data() + offset, info.DstSize, &status, &received); diff --git a/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp new file mode 100644 index 00000000..b95a8cf2 --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp @@ -0,0 +1,1978 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/Verb/VerbPort.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 VERB EMITTERS AND THE MONOLITH VERB PORT, moved from MG_Remote/Client/EmitTables.cpp +// so a library without a transport runs the record arm. One planner per verb serves both shapes: +// under a transport its record goes to the session (MG_Remote resolves an MG_Record::VerbSession), +// and on monolith's record arm to an in-process RecordVerbSink bound to the one backend object. + +#include "VerbPort.h" +#include "GpuWriteSet.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Record { + + [[noreturn]] void UnmigratedVerbFatal(const char* slot) { + // The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the + // slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is + // inert in an INFO build - and INFO is what every device lane runs. + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); + } + + // A verb that reads buffers but starts no shader: clear, blit, readback, present. The + // push still has to run - a coherent map is read by the GPU on any of them - but there + // is no shader that could write one, so no mark walk. + void BeforeReadOnlyVerb() { + MG_Record::PushPersistentMapsBeforeVerb(); + MG_Pipe::MGPipeDrainDeferredDestroys(); + } + + namespace { + + Bool g_dropClearEmission = false; + Uint64 g_droppedClearEmissions = 0; + Bool g_dropDrawEmission = false; + Uint64 g_droppedDrawEmissions = 0; + + // E2's control has to be armable from OUTSIDE the process that runs the replay, because + // the statement it makes is about a trace lane and not about a unit case: "drop an + // emission and OpenRA's SSIM falls below 0.99". A recompile would make the control + // arm against source text, which is ID-22(a)'s defect. + // + // READ WITH getenv RATHER THAN THROUGH MG_Config, DELIBERATELY AND TEMPORARILY. Config.h + // is c0's and a new MOBILEGL_IPC_* knob goes through the integrator; these are + // NEGATIVE-CONTROL switches no operator may ever set, and they announce themselves at + // warning level every time they arm so they cannot be on by accident. Flagged for + // adoption into IpcTable if the integrator wants them there. + Bool ReadControlKnob(const char* name) { + const char* value = std::getenv(name); + return value != nullptr && value[0] == '1' && value[1] == '\0'; + } + + // ---- WHY THERE ARE TWO OF THESE KNOBS, measured rather than assumed ----------------- + // + // MOBILEGL_IPC_E2_DROP_CLEAR came first and it does exactly what it says: every glClear + // stops at the client and no Clear record reaches the ring. It STILL COULD NOT TURN THE + // E2 RETRACE RED (joint-v1.md 3: SSIM 1.000000, mismatchPixels=0 with the knob armed and + // the arming WARN in the library's own log). That is not a broken knob, it is OpenRA: + // `apitrace dump` of openra.trace over the 31249 replayed calls counts 30 glClear, 30 + // glXSwapBuffers and 788 glDrawArrays, and the final frame issues its clear at call + // 30197 and then covers the surface four times over with a terrain layer + // (`glDrawArrays(GL_TRIANGLES, first=56064, count=16128)` x4, under a scissor of + // -24,-24,688x528 over a 640x480 surface) before the snapshot at 31249. A frame that + // overdraws every pixel it clears has a picture that does not depend on the clear, so + // "drop the clear" is a control whose observable is invisible to THIS trace - R-16's + // exact defect, a gate that cannot go red for its own reason. + // + // MOBILEGL_IPC_E2_DROP_DRAW is the honest form of the same statement for a trace lane: + // drop every DrawVbo record and the picture can only be the clear colour. It is the + // control that makes "the wire carried the frame" falsifiable, because the thing it + // removes is the thing the golden is made of. + // + // DROP_CLEAR IS KEPT rather than retired: it drops a real record, it now publishes the + // count it dropped (below), and a scenario whose picture DOES depend on its clear - + // ClearThenReadPixelsScenario is the reduced path's target A - is where it is + // observable. What it is no longer allowed to be is E2's retrace control. + void ArmControlKnobs() { + g_dropClearEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_CLEAR"); + g_dropDrawEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_DRAW"); + if (g_dropClearEmission) { + MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_CLEAR=1 - a NEGATIVE CONTROL is " + "armed and every glClear will be DROPPED on the wire. It is observable " + "only where the picture depends on the clear: OpenRA overdraws its whole " + "surface every frame, so this knob does NOT redden the E2 retrace " + "(measured, joint-v1.md 3) - MOBILEGL_IPC_E2_DROP_DRAW is the one that " + "does. The dropped count is published on the 'E2 control armed' line"); + } + if (g_dropDrawEmission) { + MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_DRAW=1 - E2's NEGATIVE CONTROL is " + "armed and every DrawVbo record will be DROPPED on the wire. The surface " + "can then only carry the clear colour, so this arm is expected to fail its " + "SSIM threshold; a lane that stays green with it set is not going through " + "the wire at all"); + } + } + + // ID-49's two halves, in one place so the emitter and its control read the same + // arithmetic. + // + // GL 4.6 8.4.4, pack side: the destination row stride is ROW_LENGTH (or the width) + // pixels rounded UP to PACK_ALIGNMENT, the first written byte is offset by SKIP_ROWS + // whole strides plus SKIP_PIXELS pixels, and only `width * bytesPerPixel` bytes of each + // stride are written - the gaps belong to the application and are never touched. That + // last clause is what the control checks with a sentinel. + Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel, + const PixelStoreParameters& pack) { + // SKIP_IMAGES (and IMAGE_HEIGHT) are NOT consulted: glReadPixels is a 2-D read and GL + // ignores the image-level pack parameters for it, exactly as the monolith conversion + // path does at DirectGLES.cpp:10905 (honorPackImageParams=false). A non-zero + // SkipImages therefore does not make the layout non-tight (codex 6). + if (pack.SkipRows != 0 || pack.SkipPixels != 0) return false; + if (pack.RowLength != 0 && pack.RowLength != width) return false; + const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; + const Uint64 rowBytes = static_cast(width) * bytesPerPixel; + return (rowBytes % alignment) == 0; + } + + // A band's box origin (g5-readback): the read's origin plus the band's offset, summed + // wide. It can only leave Int32 for a read whose far edge was already past INT32_MAX, + // and every pixel out there is outside any framebuffer - GL leaves their values + // undefined - so it saturates instead of wrapping onto real pixels. + Int32 ReadbackBandOrigin(GLint origin, Uint64 offset) { + const Int64 at = static_cast(origin) + static_cast(offset); + constexpr Int64 kMax = std::numeric_limits::max(); + return static_cast(at > kMax ? kMax : at); + } + + // ---- P13 W5: where a split emitter's record goes ---------------------------------- + // + // The session, resolved by MG_Remote (EmitTables.cpp installs the resolver at static init). + // A library without a transport has none: there, only the monolith verb port reaches an + // emitter, and an emitter reached without it would be a slot falling through to nothing. + VerbSessionResolver g_verbSessionResolver = nullptr; + + VerbSession& RequireVerbSession(const char* slot) { + if (g_verbSessionResolver == nullptr) { + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::NoClientSession, + "MGPipe: Fatal{NoClientSession, \"%s\"} - a verb emitter ran with neither " + "the monolith verb port installed nor a transport to carry its record", + slot); + } + return g_verbSessionResolver(slot); + } + + + // ---- P13 W4: THE MONOLITH VERB PORT ---------------------------------------------- + // + // Monolith's record arm (MG_Config::RecordArmAliasesFrontend) reads the verb's own state - + // VerbIndirectBuffer, VerbDispatchIndirectBuffer, ... - and nothing in monolith wrote it: + // ServerVerbSink is its only writer, and monolith never decoded a verb record. So the + // ported slots run THESE emitters - the same plan, the same pre-verb hooks in the same + // order (persistent-map push, owned client-memory inputs, the GPU-write marks) - and hand + // the planned record, in this process and on this thread, to a ServerVerbSink bound to + // the one backend object, which stamps the verb state exactly as the apply thread does + // and calls the backend's REAL table (ServerVerbSink::Table, never gBackendFunctionsTable, + // so nothing re-enters this port). One planner and one sink body serve both shapes: the + // monolith record arm is the split record arm minus the codec and the ring. + Bool g_monolithVerbPort = false; + + RecordVerbSink& MonolithVerbSink() { + // Leaked at exit like every other process-lifetime singleton (ID-8). + static RecordVerbSink& sink = *new RecordVerbSink(); + return sink; + } + + // The answer of a reply row on the port: written straight into the caller's buffer, which + // is what the session's reply slot would have been copied into. + class MonolithReplySink final : public MG_Pipe::MGPipeReplySink { + public: + MonolithReplySink(void* out, Uint64 capacity) : m_out(out), m_capacity(capacity) {} + void PostReply(Uint64, Int32 status, const void* bytes, Uint64 size) override { + m_posted = true; + m_status = status; + m_size = size; + if (status == kStatusOk && bytes != nullptr && m_out != nullptr && size != 0) { + std::memcpy(m_out, bytes, static_cast(std::min(size, m_capacity))); + } + } + Bool Posted() const { return m_posted; } + Int32 Status() const { return m_status; } + Uint64 Size() const { return m_size; } + + private: + void* m_out; + Uint64 m_capacity; + Bool m_posted = false; + Int32 m_status = kStatusError; + Uint64 m_size = 0; + }; + + // The port's reply cap: one band answers any read a process can hold (PlanReadbackBands + // still cuts a row wider than this, as it would for a slot). + constexpr Uint32 kMonolithMaxReplyBytes = 0x7fffffffu; + + Bool ApplyOnMonolithPort(MG_Pipe::MGPWireOp op, const void* payload, const MG_Pipe::MGPipeVerbTail* tails, + Uint32 tailCount, MG_Pipe::MGPipeReplySink* replies) { + RecordVerbSink& sink = MonolithVerbSink(); + MG_Backend::BackendObject* backend = MG_Backend::pActiveBackendObject.get(); + if (sink.Backend() != backend) { + sink.SetBackend(backend); + sink.SetMaxReplyBytes(kMonolithMaxReplyBytes); + } + using Op = MG_Pipe::MGPWireOp; + switch (op) { + case Op::DrawVbo: { + const auto& info = *static_cast(payload); + const auto* ranges = (tailCount > 0 && tails[0].Size != 0) + ? static_cast(tails[0].Bytes) + : nullptr; + const auto* indirect = ((info.Flags & MG_Pipe::kDrawIsIndirect) != 0 && tailCount > 1) + ? static_cast(tails[1].Bytes) + : nullptr; + return sink.OnDrawVbo(info, ranges, nullptr, indirect); + } + case Op::LaunchGrid: + return sink.OnLaunchGrid(*static_cast(payload)); + // ---- W4b: the texture family's verbs ---- + case Op::GenerateMipmap: + return sink.OnGenerateMipmap(*static_cast(payload)); + case Op::CopyFramebufferToTexture: + return sink.OnCopyFramebufferToTexture(*static_cast(payload)); + case Op::ResourceCopyRegion: + return sink.OnResourceCopyRegion(*static_cast(payload)); + // ---- W4b: the readbacks. The reply rows answer through `replies`, which writes the + // caller's buffer; the pack-buffer rows land in the buffer through the applier. + case Op::ReadPixels: + return sink.OnReadPixels(*static_cast(payload), 0, replies); + case Op::GetTextureImage: + return sink.OnGetTextureImage(*static_cast(payload), 0, replies); + case Op::ReadPixelsToBuffer: + return sink.OnReadPixelsToBuffer(*static_cast(payload)); + case Op::GetTextureImageToBuffer: + return sink.OnGetTextureImageToBuffer(*static_cast(payload)); + // ---- W4c: framebuffers, image units, storage-block bindings ---- + case Op::Clear: + return sink.OnClear(*static_cast(payload)); + case Op::Blit: + return sink.OnBlit(*static_cast(payload)); + case Op::BindShaderImage: + return sink.OnBindShaderImage(*static_cast(payload)); + // ---- W4a's XFB half: the capture span and its object, by lifetime id ---- + case Op::BeginStreamOutput: + return sink.OnBeginStreamOutput(*static_cast(payload)); + case Op::EndStreamOutput: + return sink.OnEndStreamOutput(*static_cast(payload)); + case Op::PauseStreamOutput: + return sink.OnPauseStreamOutput(*static_cast(payload)); + case Op::ResumeStreamOutput: + return sink.OnResumeStreamOutput(*static_cast(payload)); + case Op::BindStreamOutput: + return sink.OnBindStreamOutput(*static_cast(payload)); + case Op::DeleteStreamOutput: + return sink.OnDeleteStreamOutput(*static_cast(payload)); + case Op::SetStorageBlockBinding: { + // The name is the emitter's own NUL-terminated string (VerbChannel::StageBytes on + // the port names it by address), alive for this call. + const auto& record = *static_cast(payload); + const auto* name = reinterpret_cast(static_cast(record.Name.Offset)); + return sink.OnSetStorageBlockBinding(record, name); + } + default: + // A slot InstallMonolithVerbPort routes here whose row this switch does not know: + // the two lists drifted, and falling through to the driver would be the very + // "ran the other arm and looked fine" this port exists to end. + UnmigratedVerbFatal("MonolithVerbPort"); + } + } + + // WHERE A PORTED EMITTER'S RECORD GOES: the session's ring, or - on monolith's record arm - + // the in-process sink above. Spelled like the session (EmitAndWait / EmitAndWaitTails / + // MaxReplyBytes / ...) so an emitter reads the same either way. + class VerbChannel { + public: + explicit VerbChannel(VerbSession* session) : m_session(session) {} + + Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const void* varTail, Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, + Int32* statusOut, Uint64* replySizeOut = nullptr) { + if (m_session != nullptr) { + return m_session->EmitAndWait(op, payload, payloadBytes, varTail, varTailBytes, replyOut, + replyBytes, statusOut, replySizeOut); + } + const MG_Pipe::MGPipeVerbTail tail{varTail, varTailBytes}; + Port(op, payload, &tail, varTail != nullptr ? 1u : 0u, replyOut, replyBytes, statusOut, replySizeOut); + return 0; + } + Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const MG_Pipe::MGPipeVerbTail* tails, Uint32 tailCount, void* replyOut, Uint64 replyBytes, + Int32* statusOut, Uint64* replySizeOut = nullptr) { + if (m_session != nullptr) { + return m_session->EmitAndWaitTails(op, payload, payloadBytes, tails, tailCount, replyOut, + replyBytes, statusOut, replySizeOut); + } + Port(op, payload, tails, tailCount, replyOut, replyBytes, statusOut, replySizeOut); + return 0; + } + Uint32 MaxReplyBytes() const { + return m_session != nullptr ? m_session->MaxReplyBytes() : kMonolithMaxReplyBytes; + } + void RequireReadPixelsReplyFits(Uint32 width, Uint32 height, Uint32 format, Uint32 type, + Uint64 bytes) const { + if (m_session != nullptr) m_session->RequireReadPixelsReplyFits(width, height, format, type, bytes); + } + // A blob the record names by reference: staged in SEG_STAGE under a session, its own + // address on the port (kMGHostSpanSegNone, the monolith spelling MGPipeHostBytes reads). + MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size) { + if (m_session != nullptr) return m_session->StageBytes(bytes, size); + return MG_Pipe::MGPBlobRef{static_cast(reinterpret_cast(bytes)), size, + MG_Pipe::kMGHostSpanSegNone, 0}; + } + + private: + void Port(MG_Pipe::MGPWireOp op, const void* payload, const MG_Pipe::MGPipeVerbTail* tails, Uint32 tailCount, + void* replyOut, Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) { + MonolithReplySink replies(replyOut, replyBytes); + const Bool applied = ApplyOnMonolithPort(op, payload, tails, tailCount, &replies); + // A sink that declined without answering is the decoder's DECLINED; an answer it + // posted is the answer, whatever the return. + const Int32 status = replies.Posted() + ? replies.Status() + : (applied ? MG_Pipe::MGPipeReplySink::kStatusOk : MG_Pipe::MGPipeReplySink::kStatusDeclined); + if (statusOut != nullptr) *statusOut = status; + if (replySizeOut != nullptr) *replySizeOut = replies.Posted() ? replies.Size() : 0; + if (!applied && replyOut == nullptr) { + MGLOG_E_ONCE("MGPipe: the monolith verb port's op %u was declined by the sink; nothing was " + "drawn, dispatched or changed", + static_cast(op)); + } + } + + VerbSession* m_session; + }; + + // The channel a ported emitter uses: the port when monolith's record arm installed it + // (only the ported slots reach an emitter there), the active session otherwise. + VerbChannel ChannelFor(const char* slot) { + if (g_monolithVerbPort) return VerbChannel(nullptr); + return VerbChannel(&RequireVerbSession(slot)); + } + + // ============================================================================= + // CLASS B - the five slots the verb census measured (CONTRACT-P5.md §7) + // ============================================================================= + + void EmitClear(GLbitfield mask) { + VerbChannel session = ChannelFor("Clear"); + BeforeReadOnlyVerb(); + + if (g_dropClearEmission) { + // A negative control. Everything above still ran, so the only difference + // between this arm and the live one is the record - which is exactly the + // statement "the picture comes from the wire" that E2 exists to prove. Its + // OBSERVABILITY is a property of the workload, not of this branch: see + // ArmControlKnobs for the measurement that took E2's retrace off this knob. + ++g_droppedClearEmissions; + return; + } + + MG_Pipe::MGPClear record{}; + // The DRAW framebuffer is whatever the server's own SyncRenderState resolves from + // gPipeInputs, which the client's MGP_FILL(Clear) at GL_Drawing.cpp:534 has just + // written and the verb barrier keeps still (R-1). Naming a handle here would be a + // SECOND statement of the binding, and the second one is the one that goes stale. + record.Fbo = MG_Pipe::kMGPipeNullHandle; + record.Kind = MG_Pipe::kMGPipeClearKindWhole; + record.DrawBufferIndex = -1; + record.BufferMask = static_cast(mask); + record.ValueClass = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), nullptr, 0, + nullptr, 0, nullptr); + } + + + // ---- f1: verbatim clear/copy/mipmap records (CONTRACT-P5B §2) ---- + void EmitF1Clear(const char* slot, MG_Pipe::MGPipeHandle fbo, GLenum buffer, + GLint drawbuffer, Uint8 valueClass, const void* value, + GLfloat depth = 0, GLint stencil = 0) { + VerbChannel session = ChannelFor(slot); + BeforeReadOnlyVerb(); + MG_Pipe::MGPClear record{}; + record.Fbo = fbo; + record.DrawBufferIndex = drawbuffer; + record.ValueClass = valueClass; + switch (buffer) { + case GL_COLOR: + record.Kind = MG_Pipe::kMGPipeClearKindColor; + std::memcpy(record.ColorValue, value, sizeof(record.ColorValue)); + break; + case GL_DEPTH: + record.Kind = MG_Pipe::kMGPipeClearKindDepth; + std::memcpy(&record.DepthValue, value, sizeof(record.DepthValue)); + break; + case GL_STENCIL: + record.Kind = MG_Pipe::kMGPipeClearKindStencil; + std::memcpy(&record.StencilValue, value, sizeof(record.StencilValue)); + break; + case GL_DEPTH_STENCIL: + record.Kind = MG_Pipe::kMGPipeClearKindDepthStencil; + record.DepthValue = depth; + record.StencilValue = stencil; + break; + default: UnmigratedVerbFatal(slot); + } + session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { + EmitF1Clear("ClearBufferfv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassFloat, value); + } + void EmitClearNamedFramebufferfv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLfloat* value) { + EmitF1Clear("ClearNamedFramebufferfv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value); + } + + void EmitClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { + EmitF1Clear("ClearBufferiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassInt, value); + } + void EmitClearNamedFramebufferiv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLint* value) { + EmitF1Clear("ClearNamedFramebufferiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value); + } + + void EmitClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { + EmitF1Clear("ClearBufferuiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassUint, value); + } + void EmitClearNamedFramebufferuiv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLuint* value) { + EmitF1Clear("ClearNamedFramebufferuiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value); + } + + void EmitClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + EmitF1Clear("ClearBufferfi", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); + } + void EmitClearNamedFramebufferfi(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + EmitF1Clear("ClearNamedFramebufferfi", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); + } + const SharedPtr& F1BoundTexture(GLenum target) { + auto& ctx = *MG_State::pGLContext; + return ctx.GetTextureUnitObject(ctx.GetActiveTextureUnit()) + .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject(); + } + void EmitF1Copy(GLenum target, GLint level, GLenum format, GLint x, GLint y, + GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, Bool subImage) { + VerbChannel session = ChannelFor(subImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); + BeforeReadOnlyVerb(); + MG_Pipe::MGPCopyFromFramebuffer record{}; + record.Dst = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*F1BoundTexture(target)); + record.Target = static_cast(target); + record.Level = level; + record.InternalFormat = format; + record.X = x; record.Y = y; + record.Width = width; record.Height = height; + record.XOffset = xoffset; record.YOffset = yoffset; + record.SubImage = subImage; + session.EmitAndWait(MG_Pipe::MGPWireOp::CopyFramebufferToTexture, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + void EmitCopyTexImage2D(GLenum target, GLint level, GLenum format, GLint x, GLint y, + GLsizei width, GLsizei height, GLint) { + EmitF1Copy(target, level, format, x, y, width, height, 0, 0, false); + } + void EmitCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, + GLint x, GLint y, GLsizei width, GLsizei height) { + EmitF1Copy(target, level, 0, x, y, width, height, xoffset, yoffset, true); + } + void EmitGenerateMipmap(GLenum target) { + VerbChannel session = ChannelFor("GenerateMipmap"); + BeforeReadOnlyVerb(); + const auto& texture = F1BoundTexture(target); + MG_Pipe::MGPMipPlan record{}; + record.Res = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*texture); + record.Target = static_cast(target); + record.BaseLevel = texture->GetLevelRange().x(); + const auto* mipmap = dynamic_cast(texture.get()); + if (mipmap && !texture->GetUploadTargets().empty()) { + const auto plan = MG_Impl::GLImpl::ComputeMipmapGenerationRange(*mipmap, texture->GetUploadTargets()[0]); + // LevelCount is the logical end-exclusive, not the number of + // levels following BaseLevel. Preserve that existing carrier. + record.LevelCount = static_cast(std::min(plan.End, mipmap->GetMipmapLevelCount())); + } + session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + // All draw entry points share the owned-input preparation and emission below. + [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier); + void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, + const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, + const void* clientIndices, Uint64 clientIndexBytes, + const MG_Pipe::MGPDrawIndirect* indirect); + + // Does the bound VAO fetch any ENABLED attribute out of the application's own memory. + // The test is EmitVertexBuffers' own (`attrib.Enabled && !attrib.Buffer` is exactly what + // it publishes as Res == kMGPipeNullHandle), so the flag on the wire and the record the + // server applies agree by construction. + Bool BoundVaoHasClientVertexArrays(MG_State::GLState::GLContext* ctx) { + if (ctx == nullptr) return false; + const auto& vao = ctx->GetBoundVertexArray(); + if (!vao) return false; + const auto& attributes = vao->GetAllAttributes(); + for (SizeT i = 0; i < attributes.size(); ++i) { + if (attributes[i].Enabled && !attributes[i].Buffer) return true; + } + return false; + } + + void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) { + const Bool clientArrays = BoundVaoHasClientVertexArrays(MG_State::pGLContext.get()); + + MG_Pipe::MGPDrawInfo info{}; + info.Mode = static_cast(mode); + info.IndexSize = 0; // arrays + // NO kDrawHasUserIndices: the reduced path draws from a VBO. kDrawClientArrays IS + // set when one is present, because the wait rule is computed from the record on + // both sides and this path publishes its own record rather than PlanDrawInfo's. + info.Flags = clientArrays ? static_cast(MG_Pipe::kDrawClientArrays) : 0; + info.InstanceCount = 1; + info.StartInstance = 0; + info.RestartIndex = 0; + info.DrawIdOffset = 0; + info.IndexResource = MG_Pipe::kMGPipeNullHandle; + info.MinIndex = ~0u; // "unknown", MGPipeTypes.h:1330 + info.MaxIndex = ~0u; + info.XfbCpuCapturedVertices = 0; + info.NumDraws = 1; + + const MG_Pipe::MGPDrawRange range{static_cast(first), static_cast(count), 0}; + // One tail of exactly NumDraws entries. w1's encoder recomputes that from the + // payload and Fatals on a disagreement, on THIS side - so a NumDraws that drifted + // from the tail is a producer-side abort rather than a corrupt stream a peer has to + // diagnose. + EmitDrawRecord("DrawArrays", info, &range, 1, nullptr, 0, nullptr); + } + + // ============================================================================= + // P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots + // (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL + // call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink + // reproduces the backend call the monolith makes. + // ============================================================================= + // + // Every entry point below is: read the bindings, plan the head and the ranges (the pure + // functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb + // hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record; + // ID-18), honours the E2 draw-drop control for every draw record and not only the P5 + // one, stages a client index array into SEG_STAGE when there is one, and emits. + // + // Client arrays and client indices become owned ordinary buffer resources. + // Remaining invalid/unrepresentable draw shapes are named before encoding: + // +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect` + // would be a host pointer, which rule B forbids on the wire. + // +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has + // already raised INVALID_OPERATION for it; stated so the emitter + // cannot send a null handle where the sink dereferences one). + // +INDEX_OFFSET an element-buffer byte offset that is not a whole number of + // indices (or past 2^32 of them): the record spells Start in + // indices, and rounding would draw from the wrong element. + + [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) { + char name[96]; + std::snprintf(name, sizeof(name), "%s+%s", slot, qualifier); + UnmigratedVerbFatal(name); + } + + // The bindings the plan is made from, read ONCE per draw from the frontend context on + // the GL thread. The element buffer is the VAO's (the same slot EmitIndexBuffer read at + // validate), the two indirect buffers are the context's, and the handles are LOOKED UP, + // never minted: a push build mints in the BufferObject constructor. + RemoteDrawBindings ReadDrawBindings() { + RemoteDrawBindings b{}; + MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get(); + if (ctx == nullptr) return b; + if (const auto& vao = ctx->GetBoundVertexArray()) { + if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) { + b.ElementBufferBound = true; + b.ElementBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*bound); + } + // P5e (vi): the client-array probe, in the SAME single read of the bindings the + // rest of the plan is made from rather than in a second walk at the refusal - + // this runs on the GL thread once per draw and the refusal must not add its own + // frontend pass. The test is the emitter's own: EmitVertexBuffers publishes + // Res == kMGPipeNullHandle for exactly `attrib.Enabled && !attrib.Buffer`, so + // the flag and the record agree by construction instead of by inspection. + const auto& attributes = vao->GetAllAttributes(); + for (SizeT i = 0; i < attributes.size(); ++i) { + if (attributes[i].Enabled && !attributes[i].Buffer) { + b.ClientVertexArrays = true; + break; + } + } + } + if (const auto& di = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::DrawIndirect).GetBoundObject()) { + b.DrawIndirectBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*di); + } + if (const auto& pb = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::Parameter).GetBoundObject()) { + b.ParameterBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*pb); + } + b.PrimitiveRestart = ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + b.RestartIndex = ctx->GetPrimitiveRestartIndex(); + return b; + } + + // The one emission for all nineteen. `clientIndices`/`clientIndexBytes` name a client + // index array to stage (no element buffer bound); `indirect` is the kDrawIsIndirect + // block. The two are exclusive by construction here and by the layout on both sides. + void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, + const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, + const void* clientIndices, Uint64 clientIndexBytes, + const MG_Pipe::MGPDrawIndirect* indirect) { + VerbChannel session = ChannelFor(slot); + MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); + MG_Pipe::MGPipeDrainDeferredDestroys(); + // Snapshot before marking THIS draw's potential GPU writes: resolving + // a GPU-produced EBO here must not clear its pending mark for this draw. + UniquePtr ownedInputs; + if ((info.Flags & MG_Pipe::kDrawClientArrays) != 0 || clientIndexBytes != 0) { + if (MG_State::pGLContext == nullptr) RefuseDrawByName(slot, "NO_CONTEXT"); + ownedInputs = MakeUnique(*MG_State::pGLContext); + if (!ownedInputs->Prepare(info, ranges, numDraws, clientIndices, clientIndexBytes, indirect)) { + MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation, + MakeUnique("MG_Remote/Client", slot, + "the client vertex/index fetch range cannot be represented by its storage")); + return; + } + } + MarkGpuWritesForDraw(); + + if (g_dropDrawEmission) { + // E2's negative control covers EVERY draw record, not only DrawArrays: the + // Minecraft traces are DrawElements frames, and a control that dropped only the + // one P5 entry point would leave those lanes green with the wire disarmed. + ++g_droppedDrawEmissions; + return; + } + + info.NumDraws = numDraws; + MG_Pipe::MGPipeVerbTail tails[2] = {{ranges, static_cast(numDraws) * sizeof(MG_Pipe::MGPDrawRange)}, + {nullptr, 0}}; + Uint32 tailCount = 1; + if (indirect != nullptr) { + info.Flags |= MG_Pipe::kDrawIsIndirect; + tails[1] = {indirect, sizeof(*indirect)}; + tailCount = 2; + } + session.EmitAndWaitTails(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails, + tailCount, nullptr, 0, nullptr); + } + + // ---- the single-draw indexed family: DrawElements, DrawElementsBaseVertex, the two + // DrawRangeElements*, the four DrawElementsInstanced* ------------------------------- + void EmitIndexedDraw(const char* slot, GLenum mode, GLsizei count, GLenum type, + const void* indices, GLint baseVertex, GLsizei instanceCount, + GLuint baseInstance, Bool hasRange, GLuint start, GLuint end) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = RemoteIndexSizeFor(type); + if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + MG_Pipe::MGPDrawInfo info = + PlanDrawInfo(mode, indexSize, instanceCount, baseInstance, 1, bindings); + if (hasRange) { + info.Flags |= MG_Pipe::kDrawHasIndexRange; + info.MinIndex = start; + info.MaxIndex = end; + } + MG_Pipe::MGPDrawRange range{}; + if (!PlanDrawRange(bindings, indexSize, indices, count, baseVertex, range)) { + RefuseDrawByName(slot, "INDEX_OFFSET"); + } + // No element buffer: `indices` is the application's array and the draw's own + // range is exactly what the driver would read. A null pointer or an empty draw + // stages nothing and crosses as a zero-length range, which is what the monolith + // hands the driver too. + const Bool clientArray = !bindings.ElementBufferBound && indices != nullptr && count > 0; + EmitDrawRecord(slot, info, &range, 1, clientArray ? indices : nullptr, + clientArray ? static_cast(count) * indexSize : 0, nullptr); + } + + void EmitDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { + EmitIndexedDraw("DrawElements", mode, count, type, indices, 0, 1, 0, false, 0, 0); + } + void EmitDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLint basevertex) { + EmitIndexedDraw("DrawElementsBaseVertex", mode, count, type, indices, basevertex, 1, 0, + false, 0, 0); + } + void EmitDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, + const void* indices) { + EmitIndexedDraw("DrawRangeElements", mode, count, type, indices, 0, 1, 0, true, start, end); + } + void EmitDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, + GLenum type, const void* indices, GLint basevertex) { + EmitIndexedDraw("DrawRangeElementsBaseVertex", mode, count, type, indices, basevertex, 1, + 0, true, start, end); + } + void EmitDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, + GLsizei instancecount) { + EmitIndexedDraw("DrawElementsInstanced", mode, count, type, indices, 0, instancecount, 0, + false, 0, 0); + } + void EmitDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLint basevertex) { + EmitIndexedDraw("DrawElementsInstancedBaseVertex", mode, count, type, indices, basevertex, + instancecount, 0, false, 0, 0); + } + void EmitDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLuint baseinstance) { + EmitIndexedDraw("DrawElementsInstancedBaseInstance", mode, count, type, indices, 0, + instancecount, baseinstance, false, 0, 0); + } + void EmitDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, + const void* indices, GLsizei instancecount, + GLint basevertex, GLuint baseinstance) { + EmitIndexedDraw("DrawElementsInstancedBaseVertexBaseInstance", mode, count, type, indices, + basevertex, instancecount, baseinstance, false, 0, 0); + } + + // ---- the instanced array draws ------------------------------------------------ + void EmitArraysDraw(const char* slot, GLenum mode, GLint first, GLsizei count, + GLsizei instanceCount, GLuint baseInstance) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, instanceCount, baseInstance, 1, bindings); + MG_Pipe::MGPDrawRange range{}; + PlanDrawRange(bindings, 0, reinterpret_cast(static_cast(first)), + count, 0, range); + EmitDrawRecord(slot, info, &range, 1, nullptr, 0, nullptr); + } + void EmitDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { + EmitArraysDraw("DrawArraysInstanced", mode, first, count, instancecount, 0); + } + void EmitDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, + GLsizei instancecount, GLuint baseinstance) { + // The vertex-FETCH base instance already crossed in set_vertex_buffers::BaseInstance + // at validate (D-H1: MGP_SET_BASE_INSTANCE runs before MGP_FILL); StartInstance is + // gl_BaseInstance's value and feeds the GL call. + EmitArraysDraw("DrawArraysInstancedBaseInstance", mode, first, count, instancecount, + baseinstance); + } + + // ---- the multi-draws: MGPDrawRange[drawcount] is exactly their shape ------------- + // + // The range array is built in a scratch vector owned by the GL thread (the emitter is + // single-threaded by the ring's own SPSC contract), sized by drawcount, never held past + // the emission. A drawcount of 0 is a call that draws nothing and emits nothing: the + // frontend has already refused a negative one. + Vector& MultiDrawScratch(Uint32 count) { + static Vector& scratch = *new Vector(); + scratch.resize(count); + return scratch; + } + + void EmitMultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { + if (drawcount <= 0 || first == nullptr || count == nullptr) return; + const RemoteDrawBindings bindings = ReadDrawBindings(); + const auto n = static_cast(drawcount); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, 1, 0, n, bindings); + Vector& ranges = MultiDrawScratch(n); + for (Uint32 i = 0; i < n; ++i) { + PlanDrawRange(bindings, 0, + reinterpret_cast(static_cast(first[i])), + count[i], 0, ranges[i]); + } + EmitDrawRecord("MultiDrawArrays", info, ranges.data(), n, nullptr, 0, nullptr); + } + + void EmitMultiIndexedDraw(const char* slot, GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount, + const GLint* basevertex) { + if (drawcount <= 0 || count == nullptr || indices == nullptr) return; + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = RemoteIndexSizeFor(type); + if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + const auto n = static_cast(drawcount); + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, n, bindings); + Vector& ranges = MultiDrawScratch(n); + Vector ownedIndices; + for (Uint32 i = 0; i < n; ++i) { + if (!PlanDrawRange(bindings, indexSize, indices[i], count[i], + basevertex != nullptr ? basevertex[i] : 0, ranges[i])) { + RefuseDrawByName(slot, "INDEX_OFFSET"); + } + if (!bindings.ElementBufferBound) { + const Uint64 byteCount = static_cast(ranges[i].Count) * indexSize; + if (byteCount > std::numeric_limits::max() - ownedIndices.size() || + (byteCount != 0 && indices[i] == nullptr)) { + MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation, + MakeUnique("MG_Remote/Client", slot, "invalid client index span")); + return; + } + ranges[i].Start = static_cast(ownedIndices.size() / indexSize); + const SizeT at = ownedIndices.size(); + ownedIndices.resize(at + static_cast(byteCount)); + if (byteCount != 0) std::memcpy(ownedIndices.data() + at, indices[i], byteCount); + } + } + EmitDrawRecord(slot, info, ranges.data(), n, ownedIndices.empty() ? nullptr : ownedIndices.data(), + ownedIndices.size(), nullptr); + } + void EmitMultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount) { + EmitMultiIndexedDraw("MultiDrawElements", mode, count, type, indices, drawcount, nullptr); + } + void EmitMultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, + const GLvoid* const* indices, GLsizei drawcount, + const GLint* basevertex) { + EmitMultiIndexedDraw("MultiDrawElementsBaseVertex", mode, count, type, indices, drawcount, + basevertex); + } + + // ---- the indirect family: the block is the second tail, NumDraws is 0 ------------- + void EmitIndirectDraw(const char* slot, GLenum mode, GLenum type, const void* indirect, + GLsizei drawcount, GLsizei stride, GLintptr parameterOffset, + Bool hasParameterBuffer) { + const RemoteDrawBindings bindings = ReadDrawBindings(); + const Uint8 indexSize = type != 0 ? RemoteIndexSizeFor(type) : 0; + if (type != 0 && indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); + if (MG_Pipe::MGPipeHandleIsNull(bindings.DrawIndirectBuffer)) { + RefuseDrawByName(slot, "CLIENT_COMMANDS"); + } + if (hasParameterBuffer && MG_Pipe::MGPipeHandleIsNull(bindings.ParameterBuffer)) { + RefuseDrawByName(slot, "UNBOUND_PARAMETER"); + } + MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, 0, bindings); + const MG_Pipe::MGPDrawIndirect block = PlanDrawIndirect(bindings, indirect, drawcount, stride, + parameterOffset, hasParameterBuffer); + EmitDrawRecord(slot, info, nullptr, 0, nullptr, 0, &block); + } + void EmitDrawArraysIndirect(GLenum mode, const void* indirect) { + EmitIndirectDraw("DrawArraysIndirect", mode, 0, indirect, 1, 0, 0, false); + } + void EmitDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { + EmitIndirectDraw("DrawElementsIndirect", mode, type, indirect, 1, 0, 0, false); + } + void EmitMultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, + GLsizei stride) { + EmitIndirectDraw("MultiDrawArraysIndirect", mode, 0, indirect, drawcount, stride, 0, false); + } + void EmitMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, + GLsizei drawcount, GLsizei stride) { + EmitIndirectDraw("MultiDrawElementsIndirect", mode, type, indirect, drawcount, stride, 0, + false); + } + void EmitMultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, + GLsizei maxdrawcount, GLsizei stride) { + EmitIndirectDraw("MultiDrawArraysIndirectCount", mode, 0, indirect, maxdrawcount, stride, + drawcount, true); + } + void EmitMultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, + GLintptr drawcount, GLsizei maxdrawcount, + GLsizei stride) { + EmitIndirectDraw("MultiDrawElementsIndirectCount", mode, type, indirect, maxdrawcount, + stride, drawcount, true); + } + + void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, + GLenum filter) { + VerbChannel session = ChannelFor("BlitFramebuffer"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPBlit record{}; + // Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set + // by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the + // barrier. The named form below carries both handles after a scoped binding override. + record.ReadFbo = MG_Pipe::kMGPipeNullHandle; + record.DrawFbo = MG_Pipe::kMGPipeNullHandle; + record.SrcX0 = srcX0; + record.SrcY0 = srcY0; + record.SrcX1 = srcX1; + record.SrcY1 = srcY1; + record.DstX0 = dstX0; + record.DstY0 = dstY0; + record.DstX1 = dstX1; + record.DstY1 = dstY1; + record.Mask = static_cast(mask); + record.Filter = static_cast(filter); + session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, + nullptr, 0, nullptr); + } + + void EmitBlitNamedFramebuffer( + const SharedPtr& read, + const SharedPtr& draw, + GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + VerbChannel session = ChannelFor("BlitNamedFramebuffer"); + // P5c (hd, CONTRACT-P5C §3.3): the scoped rebind of the client's own read/draw + // binding slots is DELETED. It existed only to stage values for the server's + // binding-slot read, and that read is gone: the sink resolves both framebuffers + // from the record's handles. The validate still refreshes both framebuffers' + // emitted state (their Named set_framebuffer_state records), which is now the + // only description the server syncs from. No driver call or binding change + // happens on the client. + MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer); + BeforeReadOnlyVerb(); + MG_Pipe::MGPBlit record{}; + record.ReadFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*read); + record.DrawFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*draw); + record.SrcX0 = srcX0; record.SrcY0 = srcY0; + record.SrcX1 = srcX1; record.SrcY1 = srcY1; + record.DstX0 = dstX0; record.DstY0 = dstY0; + record.DstX1 = dstX1; record.DstY1 = dstY1; + record.Mask = static_cast(mask); + record.Filter = static_cast(filter); + session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, + nullptr, 0, nullptr); + } + + // How many bytes glReadPixels will pack for this rectangle, from the PACK half of the + // pixel-store state. + // + // ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT. + // The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips, + // alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and + // the real backend honours the live pack state, so a 4x3 RGBA8 read with + // PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote + // to byte 120. That is the shape of the joint inproc lane's two + // DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends. + // + // So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack + // state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT - + // which is the side that holds the application's pack state, and the only side that + // can - scatters those rows into the application's pointer. "OnReadPixels writes + // exactly DstSize bytes" still holds; DstSize is now a number both sides derive from + // the same three values instead of one side deriving it from state the other cannot + // see. + // + // IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split + // entries touch take the same rule with no special case, because the rule is about the + // LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to. + Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) { + const TextureInputFormat inputFormat = + MG_Util::ConvertGLEnumToTextureInputFormat(format); + const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); + const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(inputFormat, dataType); + if (bytesPerPixel == 0) { + // NOT a guess and not a zero-length reply. A format this build cannot size is a + // readback whose answer would be silently truncated, which is the one failure a + // picture comparison cannot see. + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::UnsizedReadback, "MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x " + "- the client must declare MGPReadbackInfo::DstSize and cannot size this " + "pair; P5's reduced path reads RGBA/UNSIGNED_BYTE", + static_cast(format), static_cast(type)); + } + return static_cast(bytesPerPixel); + } + + Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) { + if (width <= 0 || height <= 0) return 0; + return static_cast(width) * static_cast(height) * + ReadbackBytesPerPixel(format, type); + } + + // GL_PACK_SWAP_BYTES as the width of the byte group it reverses for `type`, 0 when there + // is nothing to reverse. One rule for both places a swap happens: the reply form swaps + // here, and the pack-buffer form (P9) carries the number to the server, which swaps there. + Uint32 ReadbackSwapGroup(GLenum type, const PixelStoreParameters& pack) { + if (!pack.SwapBytes) return 0; + const auto dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); + SizeT group = MG_Util::GetSizedTexturePixelDataTypeSize(dataType); + if (group == 0) group = MG_Util::GetBaseTexturePixelDataTypeSize(dataType); + // This packed depth/stencil type contains two independent 32-bit words. + if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) group = 4; + return group <= 1 ? 0 : static_cast(group); + } + + void ApplyReadbackByteSwap(void* pixels, Uint64 bytes, GLenum type, + const PixelStoreParameters& pack) { + const SizeT group = ReadbackSwapGroup(type, pack); + if (group == 0 || pixels == nullptr) return; + auto* data = static_cast(pixels); + for (Uint64 at = 0; at + group <= bytes; at += group) { + for (SizeT i = 0; i < group / 2; ++i) { + std::swap(data[at + i], data[at + group - 1 - i]); + } + } + } + + // M2 / codex 11: the reply the server posted is COMPLETE and OK. A short OK reply, and a + // DECLINED or ERROR reply with a zero payload, both leave the destination full of stale + // bytes; scattering or returning it is the silently truncated picture ID-47's own comment + // says an SSIM comparison cannot see, arriving through the status field rather than + // through truncation. `expected` is the record's own DstSize (CONTRACT-P5 row 23: the + // reply's exact extent). The predicate is at namespace scope (below) and exposed for the + // control, so R-16's "drive the production predicate" holds rather than a second copy of + // the rule in the test. + + // How long a declined readback waits for the control plane or the bell to say "lost" + // before it is judged a refusal. The SessionFault is sent before the decline is posted, + // so this is only ever spent on a server that refused for real. + constexpr Uint32 kDeclineVerdictMs = 1000; + + // The same predicate at the call site, with the named Fatal each failure mode owns. + // status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on + // an oversize reply, so the only failures left are a wrong status or a SHORT one. + // + // A LOST DEVICE IS NOT ONE OF THOSE FAILURES. Once the session is device-lost every verb + // is declined by EmitAndWait itself, and the GL answer for a lost context is that its + // commands do nothing: the read writes no pixel and the application learns of the loss + // from glGetGraphicsResetStatus. Dying here instead turned a server-side GPU fault into a + // crash of the client (Chrome's in-process GPU thread took the browser with it). False: + // the caller leaves the destination untouched and returns. + Bool RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) { + if (status != MG_Pipe::MGPipeReplySink::kStatusOk && MG_Pipe::MGPipeClientDeviceLost()) return false; + // THE DECLINE CAN OUTRUN THE NEWS. A server whose device was lost inside THIS readback + // latches the session - publishing its SessionFault on the control plane - and then + // declines the record; the client can read the declined answer before it has looked at + // the control plane or seen the hangup that follows. So a decline asks once more, + // briefly, before it is taken for a server without GL.ReadPixels. + if (status == MG_Pipe::MGPipeReplySink::kStatusDeclined) { + if (MG_Pipe::MGPipeClientConfirmLossAfterDecline(kDeclineVerdictMs)) return false; + } + if (status == MG_Pipe::MGPipeReplySink::kStatusError) { + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; " + "the destination is left untouched rather than filled with stale bytes"); + } + if (status == MG_Pipe::MGPipeReplySink::kStatusDeclined) { + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no " + "GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance " + "row (R-5) but a blocking readback has no pixels to return, so it is a " + "Fatal here rather than a buffer of stale bytes"); + } + if (status != MG_Pipe::MGPipeReplySink::kStatusOk) { + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ReplyStatusInvalid, "MGPipe: Fatal{ReplyStatusInvalid, \"ReadPixels\"} - unknown reply status %d", status); + } + if (replySize != expected) { + MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ReadbackReplyShort, "MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK " + "reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row " + "23's exact extent); the missing rows would otherwise be scattered as " + "whatever the destination held", + static_cast(replySize), + static_cast(expected)); + } + return true; + } + +#include "TextureReadbackEmit.inc" + + void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, + GLenum type, void* pixels) { + VerbChannel session = ChannelFor("ReadPixels"); + + // A bound PACK buffer makes pixels an offset. The owned reply is + // packed on this thread and only the requested rows are uploaded. + const auto pbo = MG_State::pGLContext != nullptr + ? MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject() + : SharedPtr{}; + + BeforeReadOnlyVerb(); + + if (width <= 0 || height <= 0) return; + const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type); + + PixelStoreParameters pack{}; + if (MG_State::pGLContext != nullptr) { + pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false); + } + ReadbackLayout layout; + const SizeT pboOffset = reinterpret_cast(pixels); + if (!CheckedReadbackLayout(width, height, 1, bytesPerPixel, pack, false, layout) || + (pbo && (pboOffset > pbo->GetSize() || layout.End > pbo->GetSize() - pboOffset))) { + RecordReadbackRangeError("ReadPixels"); + return; + } + + // P7 GATE 5 (g5-readback): A READ LARGER THAN ONE REPLY SLOT IS BANDED, NOT REFUSED. + // KHR-GL46.direct_state_access.renderbuffers_storage reads 256x512 RGBA/FLOAT = + // 2,097,152 bytes, sixteen over MaxReplyBytes, and that used to be + // Fatal{ReplyTooLarge} on every split arm while the monolith passed. Each band below + // is an ordinary read_pixels record - its own box, and a DstSize that IS its tight + // w*h*bpp extent (ID-49) - so every answer still fits one slot (ID-47) and the + // server's PH-3 bound (its tight answer against its own LinkTerms.maxReplyBytes, + // PipeApplier.cpp OnReadPixels) holds unchanged: no reply is chunked, the READ is. The cap is the link's own, read live, exactly as + // before, so a different SEG_REPLY geometry or a stream link needs no edit here. + ReadbackBandPlan plan; + if (!PlanReadbackBands(static_cast(width), static_cast(height), + bytesPerPixel, session.MaxReplyBytes(), plan)) { + // NOT EVEN ONE PIXEL FITS (or there is no reply pool at all). That is the only + // read no banding can answer, and ID-47's named refusal is kept for it - for the + // one-pixel piece that cannot be sent, so the message names what was impossible. + session.RequireReadPixelsReplyFits(1, 1, static_cast(format), + static_cast(type), bytesPerPixel); + return; + } + + // One band's source record, the same for both destinations below. + const auto bandInfo = [&](const ReadbackBand& band, Uint64 bandBytes) { + MG_Pipe::MGPReadbackInfo info{}; + info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers" + info.Box = MG_Pipe::MGPBox{ReadbackBandOrigin(x, band.FirstColumn), + ReadbackBandOrigin(y, band.FirstRow), 0, + static_cast(band.Columns), + static_cast(band.Rows), 1}; + info.Format = static_cast(format); + info.Type = static_cast(type); + info.Target = 0; + info.Level = 0; + info.DstOffset = 0; + info.DstSize = bandBytes; + return info; + }; + + // P9 (W1, CONTRACT-P9.md §1): A PACK BUFFER IS ANSWERED BY NOTHING. Each band crosses + // as read_pixels_to_buffer naming the buffer and the band's place in it - the + // layout above, per band - and the client neither waits nor copies: the server lands + // the rows, and the mark (taken first, like every producer row's) is what sends the + // next CPU read of the buffer through resource_readback. The bands are the reply + // form's for the SERVER's sake, not the wire's: the server reads each one into its + // scratch before landing it, and the reply cap is the bound it already enforces on + // that scratch (PH-3). The monolith arm is untouched - it maps the driver PBO back + // inside its own ReadPixels, as it always has. + if (pbo) { + const MG_Pipe::MGPipeHandle target = PackBufferReadbackTarget(pbo, pboOffset + layout.End); + if (!MG_Pipe::MGPipeHandleIsNull(target)) { + MarkReadPixelsPackBuffer(); + const Uint32 swapGroup = ReadbackSwapGroup(type, pack); + ForEachReadbackBand(static_cast(width), static_cast(height), plan, + [&](const ReadbackBand& band) { + MG_Pipe::MGPReadbackToBuffer record{}; + record.Src = bandInfo(band, TightReadbackBytes(static_cast(band.Columns), + static_cast(band.Rows), + format, type)); + record.Dst = target; + record.DstBase = pboOffset + layout.Start + band.FirstRow * layout.RowStride + + band.FirstColumn * bytesPerPixel; + record.RowStride = layout.RowStride; + record.ImageStride = 0; + record.SwapGroup = swapGroup; + session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixelsToBuffer, &record, + sizeof(record), nullptr, 0, nullptr, 0, nullptr); + }); + return; + } + } + + // THE COMMON CASE KEEPS THE ZERO-COPY. A neutral pack state means the destination + // layout IS the tight layout, so each band's reply lands straight in the + // application's buffer at the band's tight offset (whole-width bands and single-row + // pieces are both contiguous there). It is a fast path for the SAME bytes, not a + // second rule: ScatterReadbackBandIntoPackState is a memcpy of the band in exactly + // this case, and the control drives that function. + const Bool direct = !pbo && ReadbackPackStateIsTight(width, bytesPerPixel, pack); + // The bounce is the price of the application having asked for a layout. It is ONE + // band - at most a reply slot - rather than the whole read, and it is freed before + // this returns: R-11's rule one level out, nothing here outlives the call. + Vector bounce; + Bool pboSynced = false; + ForEachReadbackBand(static_cast(width), static_cast(height), plan, + [&](const ReadbackBand& band) { + // ONE tight-size function for production AND the control (M3 / codex 10a): the + // number the server allocates and the number the test asserts come from the + // same body, per band now. + const Uint64 bandBytes = TightReadbackBytes(static_cast(band.Columns), + static_cast(band.Rows), + format, type); + MG_Pipe::MGPReadbackInfo info = bandInfo(band, bandBytes); + + // CHECKED BEFORE THE EMISSION, not after the answer (ID-47), through S1's + // helper, for every record. The plan makes it true by construction; it stays + // because it is the one check that names the read at the call site if the plan + // and the pool ever disagree, where the server's PH-3 refusal would only answer + // ERROR on the apply thread. + session.RequireReadPixelsReplyFits(static_cast(band.Columns), + static_cast(band.Rows), + static_cast(format), + static_cast(type), bandBytes); + + Int32 status = 0; + Uint64 replySize = 0; + if (direct) { + Uint8* at = pixels == nullptr + ? nullptr + : static_cast(pixels) + + (band.FirstRow * static_cast(width) + band.FirstColumn) * + bytesPerPixel; + session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), + nullptr, 0, at, bandBytes, &status, &replySize); + // M2 / codex 11: an OK reply that arrived short, or a DECLINE/ERROR, must not + // be handed back as pixels. The Fatal aborts before the application reads the + // buffer, so the bytes EmitAndWait already copied are never observed. + if (!RequireReadbackReplyComplete(status, replySize, bandBytes)) return; + ApplyReadbackByteSwap(at, bandBytes, type, pack); + return; + } + + if (bounce.size() < bandBytes) bounce.resize(static_cast(bandBytes)); + session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, + 0, bounce.data(), bandBytes, &status, &replySize); + // BEFORE THE SCATTER, so a short or non-OK reply never reaches the application's + // pointer at all (the bounce is the only thing that held the partial bytes). + if (!RequireReadbackReplyComplete(status, replySize, bandBytes)) return; + ApplyReadbackByteSwap(bounce.data(), bandBytes, type, pack); + if (pbo) { + // pixels is an offset, never a host pointer. Upload only the requested + // rows so PACK padding and untouched bytes survive; the offsets are the + // overflow-checked layout's, the same numbers the range check accepted. + if (!pboSynced) { + pbo->SyncGpuWrites(); + pboSynced = true; + } + const Uint64 bandRowBytes = band.Columns * bytesPerPixel; + for (Uint64 row = 0; row < band.Rows; ++row) { + pbo->UploadSubData({bounce.data() + row * bandRowBytes, + static_cast(bandRowBytes)}, + pboOffset + layout.Start + + (band.FirstRow + row) * layout.RowStride + + band.FirstColumn * bytesPerPixel); + } + return; + } + ScatterReadbackBandIntoPackState(bounce.data(), pixels, width, band, bytesPerPixel, + pack); + }); + } + + // ============================================================================= + // CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block + // (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows. + // ============================================================================= + // + // RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record + // carries the GL arguments verbatim - the enums as tokens, the GL names the backend + // keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's + // ServerVerbSink reproduces the backend call the monolith makes. The backend keeps + // reading the frontend state it reads today through the BARRIER-PULLED fields of its + // verb class, which the record's own verb stamp is what makes legal. So a migration is + // one emitter here plus one sink body there, and nothing in the backend moves. + // + // THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks + // below dispatch on the GL NAME - that is the whole point of carrying it - so a handle + // is carried for P7's sake only. FindByLifetimeId answers the handle the resource + // subsystem has already published for this object and kMGPipeNullHandle when it has + // published none; Acquire would MINT one here instead, at a call site that emits no + // create record, and the server would then be handed an identity it has never seen. + // A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a + // true statement, rather than a slot nobody allocated. + + MG_Pipe::MGPipeHandle PublishedTextureHandle( + const SharedPtr& texture) { + if (!texture) return MG_Pipe::kMGPipeNullHandle; + return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture, + texture->GetLifetimeId()); + } + + // glBindImageTexture. Emitted AT THE CALL, after the frontend has written the unit's + // ImageTextureBinding and MGP_FILL(BindImageTexture) has run - the record's verb + // boundary is what makes the server's read of that binding legal. set_shader_images + // (the draw-prep set) still travels at the next validate, untouched: that record + // describes a resolved unit for the draw, this one reproduces a call. + // + // NO PRE-VERB HOOK, AND THAT IS DELIBERATE. b1's two hooks describe "the work the + // record is ABOUT TO START" - PushPersistentMapsBeforeVerb publishes bytes an + // application wrote through a coherent map, MarkGpuWrites* builds the GPU-write set. + // A bind starts no shader and reads no buffer; the dispatch that later reads this image + // is the verb that carries both hooks, and running them here as well would push the + // same maps twice per dispatch and inflate b1's per-row counters. + void EmitBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, + GLint layer, GLenum access, GLenum format) { + VerbChannel session = ChannelFor("BindImageTexture"); + + MG_Pipe::MGPImageBind record{}; + // The unit's binding is the frontend's and has just been written by the caller, so + // the texture this record names is the one the server's SyncImageTextureBinding + // will pull for the same unit. Read from MG_State::pGLContext and NOT through + // gPipeInputs: on this side of a split gPipeInputs is gPipeInputs, which is the SERVER's + // view, and the client asking it a question is how the two halves come to disagree. + if (MG_State::pGLContext != nullptr) { + record.Res = PublishedTextureHandle( + MG_State::pGLContext->GetImageTextureBinding(static_cast(unit)).Texture); + } + record.Unit = static_cast(unit); + // The GL name the application passed, verbatim - what the ES slot is handed as + // `texture` and currently ignores. Never an identity (ARCHITECTURE 4.2.1). + record.GlName = static_cast(texture); + record.Level = static_cast(level); + record.Layer = static_cast(layer); + // THE GL ACCESS TOKEN, not MGPImageView::Access's three-value encoding (table 0's + // MGPImageBind::Access row). Two records, two jobs. + record.Access = static_cast(access); + record.Format = static_cast(format); + record.Layered = layered != GL_FALSE ? 1 : 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::BindShaderImage, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + // glDispatchCompute. THE HOOK ORDER IS b1's AND IS INHERITED FROM THE CLASS-C STUB + // VERBATIM: push the persistent maps (they produce resource_subdata records that must + // precede the verb on SEG_CMD), then the dispatch mark walk, then the record. The stub + // carried both calls before its Fatal precisely so that the package which flipped this + // slot would inherit a call site that was already correct. + void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { + VerbChannel session = ChannelFor("DispatchCompute"); + MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); + MarkGpuWritesForDispatch(); + + MG_Pipe::MGPGridInfo record{}; + record.GridX = static_cast(numGroupsX); + record.GridY = static_cast(numGroupsY); + record.GridZ = static_cast(numGroupsZ); + // Block* STAY 0 IN P5b (contract i1): the local size is a link artifact the backend + // reads from its own program, and a client-minted copy would be a second statement + // of it. P7's Magma may fill it from the reflection archive. + record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; + record.IndirectOffset = 0; + record.IsIndirect = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, + nullptr, 0, nullptr); + } + + void EmitDispatchComputeIndirect(GLintptr indirect) { + VerbChannel session = ChannelFor("DispatchComputeIndirect"); + MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); + MarkGpuWritesForDispatch(); + + MG_Pipe::MGPGridInfo record{}; + // The counts come from the GL_DISPATCH_INDIRECT_BUFFER, so the three grid fields are + // 0 and IsIndirect is what says so; the sink dispatches on it and calls + // glDispatchComputeIndirect with the offset the application spelled. + record.IsIndirect = 1; + record.IndirectOffset = static_cast(indirect); + record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; + if (MG_State::pGLContext != nullptr) { + const auto& bound = + MG_State::pGLContext + ->GetBufferBindingSlot(::MobileGL::BufferTarget::DispatchIndirect) + .GetBoundObject(); + if (bound) { + record.IndirectBuffer = MG_Pipe::MGPipeSlots().FindByLifetimeId( + MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId()); + } + } + session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, + nullptr, 0, nullptr); + } + + // glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has + // already validated them and already folds glTextureBarrier onto the same field + // (GL_Drawing.cpp:920-937), and Espryt's atomic-counter lowering - the counter bit + // implying the storage bit - stays inside the backend where the reason for it lives + // (DirectGLES.cpp:8837). A client that pre-lowered would be answering a driver question + // from the wrong side and the two arms would stop being byte-identical. + // + // No pre-verb hook: a barrier orders memory the GPU already holds. It starts no shader + // and reads no mapped buffer. + void EmitMemoryBarrier(GLbitfield barriers) { + VerbChannel session = ChannelFor("MemoryBarrier"); + MG_Pipe::MGPMemoryBarrier record{}; + record.Bits = static_cast(barriers); + record.ByRegion = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, + 0, nullptr, 0, nullptr); + } + + void EmitMemoryBarrierByRegion(GLbitfield barriers) { + VerbChannel session = ChannelFor("MemoryBarrierByRegion"); + MG_Pipe::MGPMemoryBarrier record{}; + record.Bits = static_cast(barriers); + record.ByRegion = 1; + session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, + 0, nullptr, 0, nullptr); + } + + // glCopyImageSubData -> resource_copy_region (53), which P5b rules is glCopyImageSubData + // ONLY (contract §6.4; the framebuffer-sourced copies are f1's row 76). + void EmitCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget, + GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, + const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget, + GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, + GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { + VerbChannel session = ChannelFor("CopyImageSubData"); + + // The GL target selects the texture or renderbuffer handle namespace. + MG_Pipe::MGPCopyRegion record{}; + const auto handle = [](const MG_Backend::CopyImageEndpoint& endpoint) { + return endpoint.IsRenderbuffer() + ? MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Renderbuffer, + endpoint.Renderbuffer->GetLifetimeId()) + : PublishedTextureHandle(endpoint.Texture); + }; + record.Src = handle(src); + record.Dst = handle(dst); + // GL names remain diagnostic only; backend identity comes from handles. + record.SrcGlName = + src.IsRenderbuffer() ? src.Renderbuffer->GetExternalIndex() : src.Texture->GetExternalIndex(); + record.DstGlName = + dst.IsRenderbuffer() ? dst.Renderbuffer->GetExternalIndex() : dst.Texture->GetExternalIndex(); + // The GL targets verbatim, in a Uint16 - every GL texture target fits one. NOT + // MGPipeResourceTarget: the sink only ever forwards these to a slot that takes GL + // enums, and the tree has no resource-target -> GL-enum inverse to spend on them. + record.SrcTarget = static_cast(srcTarget); + record.DstTarget = static_cast(dstTarget); + record.SrcLevel = static_cast(srcLevel); + record.DstLevel = static_cast(dstLevel); + // SrcBox is {srcX, srcY, srcZ, w, h, d}: the source origin AND the extent, which is + // one extent for both endpoints (GL spells the copy's size once). + record.SrcBox = MG_Pipe::MGPBox{srcX, srcY, srcZ, static_cast(srcWidth), + static_cast(srcHeight), + static_cast(srcDepth)}; + record.DstX = dstX; + record.DstY = dstY; + record.DstZ = dstZ; + session.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCopyRegion, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + // glShaderStorageBlockBinding -> set_storage_block_binding (75), the ONE content-carrying + // row P5b adds. The block is NAMED, not indexed, because the application's index is the + // frontend interface-query enumeration's and no backend shares that index space + // (BackendObject.h:216-221) - the name is the one coordinate all three agree on. + void EmitShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, + GLuint storageBlockBinding) { + VerbChannel session = ChannelFor("ShaderStorageBlockBinding"); + // The backend slot's own first line (DirectGLES.cpp:9201), kept here so a null name + // never becomes a zero-size blob - which rule A forbids spelling at all. + if (storageBlockName == nullptr) return; + BeforeReadOnlyVerb(); + + MG_Pipe::MGPStorageBlockBinding record{}; + record.GlName = static_cast(program); + record.Binding = static_cast(storageBlockBinding); + record.ShaderCso = MG_Pipe::kMGPipeNullHandle; + if (MG_State::pGLContext != nullptr) { + const auto& programObject = MG_State::pGLContext->GetProgramObject(program); + if (programObject) { + // The application may rebind a stage program before it is + // current or attached to a pipeline. A minted slot alone does + // not publish its archive; this verb must follow that birth. + programObject->JoinLinkAndSpirv(); + Uint64 bytes = 0; + auto& emitter = MG_Pipe::MGPipeProgramEmitterInstance(); + record.ShaderCso = emitter.AcquireShaderCso(*programObject, bytes); + emitter.EmitProgramBindings(*programObject, record.ShaderCso); + } + } + // Size = strlen + 1: THE NUL TRAVELS (contract table 0's block-name row). The + // decoder re-terminates into a bounded local and refuses a run whose last byte is + // not NUL, so the two sides agree on where the name ends. + const Uint64 nameBytes = static_cast(std::strlen(storageBlockName)) + 1ull; + record.Name = session.StageBytes(storageBlockName, nameBytes); + session.EmitAndWait(MG_Pipe::MGPWireOp::SetStorageBlockBinding, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + // CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the + // tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2). + // ============================================================================= + // + // SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a + // control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets + // the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(), + // which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no + // shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE + // TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span, + // after the record and before GLContext::EndTransformFeedback clears the live bindings, + // which is exactly where b1 already put it (GL_Drawing.cpp's + // MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same + // buffers twice and taking it INSTEAD of there would mark nothing. + // + // P5f fe extends Begin with the immutable capture snapshot: program CSO handle, + // XFB object lifetime id, and four buffer handle/range pairs. Buffer resource records + // precede this verb through BeforeReadOnlyVerb; AcquireShaderCso below publishes the + // program archive before Begin. Deferred driver Begin and End therefore need no + // frontend program or binding-point lookup. set_stream_output_targets stays unused: + // this state changes at the capture-span boundary, so it rides Begin itself. + + void EmitBeginTransformFeedback(GLenum primitiveMode) { + VerbChannel session = ChannelFor("BeginTransformFeedback"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPStreamOutputBegin record{}; + // The GL token verbatim (contract table 0's "GL enums on the wire"): the sink hands + // it to the backend slot that takes it, and nothing between here and there reads it. + record.PrimitiveMode = static_cast(primitiveMode); + auto& context = *MG_State::pGLContext; + record.LifetimeId = context.GetBoundTransformFeedbackLifetimeId(); + const auto& program = context.GetTransformFeedbackProgram(); + if (program) { + Uint64 bytes = 0; + record.CaptureProgram = MG_Pipe::MGPipeProgramEmitterInstance().AcquireShaderCso(*program, bytes); + } + static_assert(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS == 4); + for (Uint i = 0; i < 4; ++i) { + const auto& point = context.GetBufferBindingPoint(BufferTarget::TransformFeedback, i); + const auto& buffer = point.GetBoundObject(); + if (!buffer) continue; + const auto range = point.GetRange(); + const SizeT start = std::min(range.start, buffer->GetSize()); + const SizeT end = std::min(range.end, buffer->GetSize()); + record.Targets[i] = {MG_Pipe::MGPipeResourceTrackerInstance().Find(*buffer), start, + end > start ? end - start : 0}; + } + session.EmitAndWait(MG_Pipe::MGPWireOp::BeginStreamOutput, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitEndTransformFeedback() { + VerbChannel session = ChannelFor("EndTransformFeedback"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPXfbAccounting record{}; + // THE ACCOUNTING IS THE CLIENT'S OWN AND IT IS INFORMATIONAL ON THIS SIDE OF THE + // WIRE: end_stream_output's backend call takes no arguments, and the three numbers + // are what the frontend has counted over this span (CONTRACT-P5B.md §2 t2, the + // companions row). They travel because the row has carried them since P4a and + // because they are what a server-side scatter would need when P9 lands one; the + // sink today calls GL.EndTransformFeedback() and reads none of them. Read here, + // BEFORE GLContext::EndTransformFeedback resets the counters at the call site. + const auto& context = *MG_State::pGLContext; + record.CapturedVertices = context.GetTransformFeedbackCapturedVertices(); + record.PrimitivesWritten = context.GetTransformFeedbackPrimitiveCounter(); + record.PrimitiveMode = static_cast(context.GetTransformFeedbackPrimitiveMode()); + session.EmitAndWait(MG_Pipe::MGPWireOp::EndStreamOutput, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitPauseTransformFeedback() { + VerbChannel session = ChannelFor("PauseTransformFeedback"); + BeforeReadOnlyVerb(); + + // Reserved IS zero and the contract says so (MGPStreamOutputControl{Reserved = 0}). + // The row exists to BE the verb boundary - the stamp the server puts up before the + // sink runs - not to carry anything. + MG_Pipe::MGPStreamOutputControl record{}; + record.Reserved = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::PauseStreamOutput, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitResumeTransformFeedback() { + VerbChannel session = ChannelFor("ResumeTransformFeedback"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPStreamOutputControl record{}; + record.Reserved = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::ResumeStreamOutput, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitBindTransformFeedback(GLuint name) { + VerbChannel session = ChannelFor("BindTransformFeedback"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPStreamOutputBind record{}; + // THE GL NAME IS NOT AN IDENTITY (ARCHITECTURE 4.2.1) and is carried anyway, because + // it is the key the backend has always used: Espryt indexes its driver objects by it + // (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and generates the ES object on + // first bind. Name 0 is the default object, which is why the field is not a handle. + record.GlName = static_cast(name); + // Beside it, the identity that WILL dispatch: the frontend's per-object lifetime id, + // process-wide and never reused, which is what survives glGenTransformFeedbacks + // recycling a name. Read AFTER GLContext::BindTransformFeedbackObject at the call + // site, so it is the id of the object being bound and not of the previous one. + record.LifetimeId = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId(); + session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitPatchParameteri(GLenum pname, GLint value) { + VerbChannel session = ChannelFor("PatchParameteri"); + BeforeReadOnlyVerb(); + + MG_Pipe::MGPPatchParameter record{}; + // GL_PATCH_VERTICES is the only pname that reaches a backend slot - the frontend + // answers GL_PATCH_DEFAULT_*_LEVEL itself and bakes those into the synthesized + // control stage - and the frontend has already rejected every other pname with + // INVALID_ENUM before this call (GL_Drawing.cpp's PatchParameteri). Carried verbatim + // so the sink reproduces the call rather than a reading of it. + record.Pname = static_cast(pname); + record.Value = static_cast(value); + // set_patch_state (43) STILL TRAVELS, at the next validate, and that is not a + // duplicate: it is the applier's working-block copy, this is the driver push Espryt + // does AT THE CALL (DirectGLES.cpp:8760), and both pushes happen today on the + // monolith path too (CONTRACT-P5B.md §2 t2). + session.EmitAndWait(MG_Pipe::MGPWireOp::PatchParameter, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitDeleteTransformFeedback(GLuint name) { + VerbChannel session = ChannelFor("DeleteTransformFeedback"); + BeforeReadOnlyVerb(); + const auto& context = *MG_State::pGLContext; + const MG_Pipe::MGPStreamOutputBind object{name, 0, context.GetTransformFeedbackLifetimeId(name)}; + session.EmitAndWait(MG_Pipe::MGPWireOp::DeleteStreamOutput, &object, sizeof(object), + nullptr, 0, nullptr, 0, nullptr); + if (context.GetBoundTransformFeedbackName() == name) { + const MG_Pipe::MGPStreamOutputBind fallback{0, 0, context.GetTransformFeedbackLifetimeId(0)}; + session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &fallback, sizeof(fallback), + nullptr, 0, nullptr, 0, nullptr); + } + + } // namespace + + // THE CONTROL'S OWN EVIDENCE LINE, and it is emitted per frame rather than at teardown + // on purpose: a retrace that is killed by its own timeout, or whose library never runs + // MobileGL::Destroy, would leave a teardown-only line absent and the control would then + // have to accept a bare threshold failure - which is the thing R-16 forbids. One line + // per Present, only while a knob is armed, is bounded by the frame count and present + // whatever happens afterwards. + void LogE2ControlLine(Uint64 frameOrdinal) { + if (!g_dropClearEmission && !g_dropDrawEmission) return; + MGLOG_W("MGPipe: E2 control armed - drop-draw=%d drop-clear=%d, %llu records dropped " + "on the wire (draw=%llu clear=%llu), frame %llu", + g_dropDrawEmission ? 1 : 0, g_dropClearEmission ? 1 : 0, + static_cast(g_droppedDrawEmissions + g_droppedClearEmissions), + static_cast(g_droppedDrawEmissions), + static_cast(g_droppedClearEmissions), + static_cast(frameOrdinal)); + } + + + void ArmVerbControlKnobs() { ArmControlKnobs(); } + + void SetVerbSessionResolver(VerbSessionResolver resolver) { g_verbSessionResolver = resolver; } + + void AssignVerbEmitters(MG_Backend::GlobalBackendFunctionsTable& table) { + table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback; + table.GL.Clear = &EmitClear; + table.GL.DrawArrays = &EmitDrawArrays; + table.GL.DrawElements = &EmitDrawElements; + table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; + table.GL.DrawRangeElements = &EmitDrawRangeElements; + table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; + table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; + table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; + table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; + table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; + table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; + table.GL.MultiDrawArrays = &EmitMultiDrawArrays; + table.GL.MultiDrawElements = &EmitMultiDrawElements; + table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; + table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; + table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; + table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; + table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; + table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; + table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; + table.GL.ReadPixels = &EmitReadPixels; + table.GL.BlitFramebuffer = &EmitBlitFramebuffer; + table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer; + table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback; + table.GL.EndTransformFeedback = &EmitEndTransformFeedback; + table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback; + table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback; + table.GL.BindTransformFeedback = &EmitBindTransformFeedback; + table.GL.PatchParameteri = &EmitPatchParameteri; + table.GL.ClearBufferfi = &EmitClearBufferfi; + table.GL.ClearBufferfv = &EmitClearBufferfv; + table.GL.ClearBufferiv = &EmitClearBufferiv; + table.GL.ClearBufferuiv = &EmitClearBufferuiv; + table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; + table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; + table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; + table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; + table.GL.CopyTexImage2D = &EmitCopyTexImage2D; + table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; + table.GL.GenerateMipmap = &EmitGenerateMipmap; + table.GL.GetTexImage = &EmitGetTexImage; + table.GL.GetTextureImage = &EmitGetTextureImage; + table.GL.BindImageTexture = &EmitBindImageTexture; + table.GL.DispatchCompute = &EmitDispatchCompute; + table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect; + table.GL.MemoryBarrier = &EmitMemoryBarrier; + table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion; + table.GL.CopyImageSubData = &EmitCopyImageSubData; + table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; + table.GL.DrawElementsInstancedBaseVertexBaseInstance = &EmitDrawElementsInstancedBaseVertexBaseInstance; + } + + void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table) { + // P13 W4a: the draw and dispatch families. Later W4 steps add their verbs here AND to + // ApplyOnMonolithPort's switch, in the same commit. + table.GL.DrawArrays = &EmitDrawArrays; + table.GL.DrawElements = &EmitDrawElements; + table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; + table.GL.DrawRangeElements = &EmitDrawRangeElements; + table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; + table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; + table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; + table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; + table.GL.DrawElementsInstancedBaseVertexBaseInstance = &EmitDrawElementsInstancedBaseVertexBaseInstance; + table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; + table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; + table.GL.MultiDrawArrays = &EmitMultiDrawArrays; + table.GL.MultiDrawElements = &EmitMultiDrawElements; + table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; + table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; + table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; + table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; + table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; + table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; + table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; + table.GL.DispatchCompute = &EmitDispatchCompute; + table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect; + // P13 W4b: the texture family - its record arm resolves the texture from the verb's own + // handle (VerbMipRes, VerbCopyTexDst, the copy's two endpoints). + table.GL.GenerateMipmap = &EmitGenerateMipmap; + table.GL.CopyTexImage2D = &EmitCopyTexImage2D; + table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; + table.GL.CopyImageSubData = &EmitCopyImageSubData; + table.GL.ReadPixels = &EmitReadPixels; + table.GL.GetTexImage = &EmitGetTexImage; + table.GL.GetTextureImage = &EmitGetTextureImage; + // P13 W4c: framebuffer verbs name their framebuffers by handle (MGPClear::Fbo, the blit's + // pair), image binds and storage-block bindings carry their objects' handles. + table.GL.Clear = &EmitClear; + table.GL.ClearBufferfi = &EmitClearBufferfi; + table.GL.ClearBufferfv = &EmitClearBufferfv; + table.GL.ClearBufferiv = &EmitClearBufferiv; + table.GL.ClearBufferuiv = &EmitClearBufferuiv; + table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; + table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; + table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; + table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; + table.GL.BlitFramebuffer = &EmitBlitFramebuffer; + table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer; + table.GL.BindImageTexture = &EmitBindImageTexture; + table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; + // The XFB family: the span's capture program and targets ride Begin; the object is named + // by lifetime id. It follows the program family because Begin names the capture program + // by its shader CSO, whose archive monolith records carry from W4c. + table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback; + table.GL.EndTransformFeedback = &EmitEndTransformFeedback; + table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback; + table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback; + table.GL.BindTransformFeedback = &EmitBindTransformFeedback; + table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback; + g_monolithVerbPort = true; + } + + Bool MonolithVerbPortInstalled() { return g_monolithVerbPort; } + + void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; } + Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; } + + void SetDropDrawEmissionForNegativeControl(Bool drop) { g_dropDrawEmission = drop; } + Uint64 DroppedDrawEmissions() { return g_droppedDrawEmissions; } + + Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel, + const PixelStoreParameters& pack) { + return ReadbackPackStateIsTight(width, bytesPerPixel, pack); + } + + Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type) { + return TightReadbackBytes(width, height, format, type); + } + + // M2 / codex 11's predicate at namespace scope: the one RequireReadbackReplyComplete decides + // on and the one the control drives. 0=OK / 1=DECLINED / 2=ERROR. + Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected) { + return status == MG_Pipe::MGPipeReplySink::kStatusOk && replySize == expected; + } + + // g5-readback's plan (EmitTables.h, ReadbackBand). Pixels-per-reply first and the row test + // against it, so no product here can overflow whatever the inputs: a unit control drives + // this with Uint64 extremes, and the emitter with a GLsizei width. + Bool PlanReadbackBands(Uint64 width, Uint64 height, Uint64 bytesPerPixel, Uint64 maxReplyBytes, + ReadbackBandPlan& plan) { + if (width == 0 || height == 0 || bytesPerPixel == 0 || maxReplyBytes < bytesPerPixel) { + return false; + } + const Uint64 pixelsPerReply = maxReplyBytes / bytesPerPixel; // >= 1 + if (width <= pixelsPerReply) { + // floor(floor(cap / bpp) / width) == floor(cap / (bpp * width)): the most whole rows + // one reply holds, and at least one because a row fits. + plan.RowsPerBand = pixelsPerReply / width; + plan.ColumnsPerBand = width; + } else { + // A SINGLE ROW IS LARGER THAN A REPLY (a >131071-pixel RGBA/FLOAT row at the 2 MiB + // slot - wider than any attachment, but a legal glReadPixels whose in-bounds pixels + // GL still defines). Each row is cut into pieces; one row per band keeps every piece + // contiguous in the tight layout. + plan.RowsPerBand = 1; + plan.ColumnsPerBand = pixelsPerReply; + } + return true; + } + + // ID-49's scatter, for one band of a banded read (g5-readback). The whole-read scatter below + // is this function over the single band {0, height, 0, width}, so there is one copy of + // 8.4.4's arithmetic and the control drives it. + void ScatterReadbackBandIntoPackState(const void* band, void* destination, GLsizei width, + const ReadbackBand& where, Uint64 bytesPerPixel, + const PixelStoreParameters& pack) { + if (band == nullptr || destination == nullptr || width <= 0 || where.Rows == 0 || + where.Columns == 0) { + return; + } + const Uint64 rowPixels = + pack.RowLength > 0 ? static_cast(pack.RowLength) : static_cast(width); + const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; + const Uint64 strideBytes = + ((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment; + // m6: the client re-derives GL 4.6 8.4.4's pack layout, so it honours GL_PACK_ROW_LENGTH + // VERBATIM - including the ill-formed 0 < ROW_LENGTH < width, where the row stride is + // narrower than a written row and consecutive rows overlap. GL leaves that case to the + // implementation; the client reproduces exactly what the monolith backend's own scatter + // would do with the same state rather than clamping, so the two arms stay byte-identical. + // The fast path (ReadbackPackStateIsTight) already rejects any ROW_LENGTH != width, so + // this only runs on the scatter path the application asked for. + const Uint64 writtenPerRow = where.Columns * bytesPerPixel; + // SKIP_IMAGES and IMAGE_HEIGHT ARE IGNORED (codex 6). glReadPixels is a 2-D read; GL + // does not apply the image-level pack parameters to it, and the monolith conversion path + // says so explicitly with honorPackImageParams=false (DirectGLES.cpp:10905). Applying + // SKIP_IMAGES here shifted a read with SKIP_IMAGES=1 by a whole image and overran an + // application buffer sized for exactly `height` rows. Only SKIP_ROWS and SKIP_PIXELS - + // the 2-D skips - offset the first written byte; the band's own row and column then + // offset it within the read, on the SAME stride, so a banded read writes exactly the + // bytes the whole-read scatter would have, in the same row order. + auto* out = static_cast(destination) + + (static_cast(pack.SkipRows) + where.FirstRow) * strideBytes + + (static_cast(pack.SkipPixels) + where.FirstColumn) * bytesPerPixel; + const auto* in = static_cast(band); + for (Uint64 row = 0; row < where.Rows; ++row) { + std::memcpy(out + row * strideBytes, in + row * writtenPerRow, + static_cast(writtenPerRow)); + } + } + + // ID-49's scatter. Exported for the same reason as the refusal above: the control drives + // THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic. + void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width, + GLsizei height, Uint64 bytesPerPixel, + const PixelStoreParameters& pack) { + if (tight == nullptr || destination == nullptr || width <= 0 || height <= 0) return; + ScatterReadbackBandIntoPackState( + tight, destination, width, + ReadbackBand{0, static_cast(height), 0, static_cast(width)}, + bytesPerPixel, pack); + } + + // ============================================================================= + // P5b d1 - the draw family's record plan, at namespace scope so the unit cases drive + // exactly what the emitters above call (R-16). + // ============================================================================= + + Uint8 RemoteIndexSizeFor(GLenum indexType) { + switch (indexType) { + case GL_UNSIGNED_BYTE: return 1; + case GL_UNSIGNED_SHORT: return 2; + case GL_UNSIGNED_INT: return 4; + default: return 0; + } + } + + MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, + GLuint baseInstance, Uint32 numDraws, + const RemoteDrawBindings& bindings) { + MG_Pipe::MGPDrawInfo info{}; + info.Mode = static_cast(mode); + info.IndexSize = indexSize; + // The restart state rides verbatim (informational in P5b: the backend's + // ScopedRestartIndexSubstitution reads its own barrier-pulled copy and the index bytes + // on its side). kDrawHasUserIndices / kDrawIsIndirect are added by the emission itself, + // kDrawHasIndexRange by the two DrawRangeElements* callers. + info.Flags = bindings.PrimitiveRestart ? static_cast(MG_Pipe::kDrawPrimitiveRestart) : 0; + // P5e (vi), CONTRACT-P5E §2.1 (ii) / §5.1. On the wire BECAUSE the server needs it: the + // barriered predicate is computed identically by both roles from the record alone, and + // "does this draw fetch from client memory" is not derivable from anything else in it - + // a null Res inside the vertex-buffer window is also what a DISABLED attribute below + // the high-water mark publishes. Set for every draw shape, not just the array ones: a + // glDrawElements can fetch its VERTICES from client memory too. + if (bindings.ClientVertexArrays) info.Flags |= static_cast(MG_Pipe::kDrawClientArrays); + // A negative count has already been refused by the frontend (INVALID_VALUE); 0 crosses + // as 0 and draws nothing, which is what the driver does with it. + info.InstanceCount = instanceCount > 0 ? static_cast(instanceCount) : 0u; + info.StartInstance = baseInstance; + info.RestartIndex = bindings.RestartIndex; + info.DrawIdOffset = 0; + // The handle beside the call (rule D): the VAO's element buffer for an indexed draw, + // the same handle set_index_buffer (32) carried at validate; null for arrays and for a + // client index array. + info.IndexResource = (indexSize != 0 && bindings.ElementBufferBound) ? bindings.ElementBuffer + : MG_Pipe::kMGPipeNullHandle; + info.MinIndex = ~0u; // "unknown" (MGPipeTypes.h); the DrawRangeElements* callers fill it + info.MaxIndex = ~0u; + info.XfbCpuCapturedVertices = 0; + info.NumDraws = numDraws; + return info; + } + + Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, + GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out) { + out = MG_Pipe::MGPDrawRange{}; + out.Count = count > 0 ? static_cast(count) : 0u; + if (indexSize == 0) { + // Arrays: `first`, spelled through the pointer parameter so one function serves + // both shapes. A negative first has already been refused by the frontend. + const auto first = static_cast(reinterpret_cast(indicesOrFirst)); + out.Start = first > 0 ? static_cast(first) : 0u; + out.IndexBias = 0; + return true; + } + out.IndexBias = baseVertex; + if (!bindings.ElementBufferBound) { + // A client index array: the emitter stages the bytes and the run starts at its + // first index. + out.Start = 0; + return true; + } + // An element buffer: `indices` is a byte offset into it, and Start is that offset in + // INDICES - the same arithmetic OnDrawVbo inverts (offset = Start * IndexSize). An + // offset that is not a whole number of indices cannot be spelled and is the caller's + // refusal, not a rounding. + const auto offset = reinterpret_cast(indicesOrFirst); + if (offset % indexSize != 0) return false; + const std::uintptr_t start = offset / indexSize; + if (start > 0xFFFFFFFFull) return false; + out.Start = static_cast(start); + return true; + } + + MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, + GLsizei drawCount, GLsizei stride, + GLintptr parameterOffset, Bool hasParameterBuffer) { + MG_Pipe::MGPDrawIndirect block{}; + block.Buffer = bindings.DrawIndirectBuffer; + block.ParameterBuffer = hasParameterBuffer ? bindings.ParameterBuffer : MG_Pipe::kMGPipeNullHandle; + // The command byte offset the call passed as `indirect` (a bound GL_DRAW_INDIRECT_BUFFER + // makes it an offset, never an address - the emitter refused the other case by name). + block.Offset = static_cast(reinterpret_cast(indirect)); + block.ParameterOffset = hasParameterBuffer ? static_cast(parameterOffset) : 0u; + // 0 = tightly packed, as GL spells it; the backend normalises. Negative counts and + // strides have already been refused by the frontend. + block.Stride = stride > 0 ? static_cast(stride) : 0u; + block.DrawCount = drawCount > 0 ? static_cast(drawCount) : 0u; + return block; + } + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_Impl/Pipe/Verb/VerbPort.h b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.h new file mode 100644 index 00000000..39c7a4d8 --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/Verb/VerbPort.h @@ -0,0 +1,255 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/Verb/VerbPort.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 ARM'S VERB EMITTERS, WITHOUT A TRANSPORT. The draws, clears, blits, readbacks, +// compute, copies, image binds, XFB and mipmaps are planned here once. Under a transport a planned +// record goes to the session, which MG_Remote hands over as a VerbSession (EmitTables.cpp installs +// the resolver at static init); on monolith's record arm it goes to an in-process RecordVerbSink +// bound to the one backend object (InstallMonolithVerbPort). Nothing here names MG_Remote. + +#pragma once +#include + +#include +#include +#include +#include + +namespace MobileGL::MG_Record { + + // Where a split emitter's record goes: the transport's session, spelled as the emitters call + // it. Only MG_Remote implements it; a library without a transport never resolves one. + class VerbSession { + public: + virtual Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const void* varTail, Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, + Int32* statusOut, Uint64* replySizeOut) = 0; + virtual Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, + const MG_Pipe::MGPipeVerbTail* tails, Uint32 tailCount, void* replyOut, + Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) = 0; + virtual Uint32 MaxReplyBytes() const = 0; + virtual void RequireReadPixelsReplyFits(Uint32 width, Uint32 height, Uint32 format, Uint32 type, + Uint64 bytes) const = 0; + // A blob the record names by reference, staged where the record's reader can find it. + virtual MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size) = 0; + + protected: + ~VerbSession() = default; + }; + + // The session for `slot`, or a named death when there is none (the resolver's own refusal). + using VerbSessionResolver = VerbSession& (*)(const char* slot); + void SetVerbSessionResolver(VerbSessionResolver resolver); + + // Every slot a verb emitter serves, written into `table` (the remote table's class-B verbs). + void AssignVerbEmitters(MG_Backend::GlobalBackendFunctionsTable& table); + + // A verb that reads buffers but starts no shader (clear, blit, readback, present, fences): the + // persistent-map push and the deferred destroys, before its record. + void BeforeReadOnlyVerb(); + + // Arms the E2 negative controls from their environment knobs (once, at table build). + void ArmVerbControlKnobs(); + // The E2 control's per-frame evidence line, emitted only while a control is armed. + void LogE2ControlLine(Uint64 frameOrdinal); + + // Called by the Fatal slots. Named separately so a death test can filter on it and so + // that the message wording lives in exactly one place. + [[noreturn]] void UnmigratedVerbFatal(const char* slot); + + // P13 W4: monolith's record arm. Overwrites the ported verb slots of the backend's own + // table with this file's emitters, which hand their planned record to an in-process + // RecordVerbSink instead of a session (VerbPort.cpp, "THE MONOLITH VERB PORT"). Installed + // by the library's bring-up when MG_Config::RecordArmAliasesFrontend() holds. + void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table); + Bool MonolithVerbPortInstalled(); + + // THE E2 NEGATIVE CONTROLS (t1's debt against c1, BRIEF §7). When set, the named emitter + // SKIPS its record - it still runs the pre-verb hooks and still returns - so a replay that + // is really going through the wire loses that verb while a replay that fell through to the + // driver is unaffected. They are functions rather than knobs in Config.h for two reasons: + // a control has to be settable from a test process that has already started, and a knob + // would be a MOBILEGL_IPC_-shaped name for something no operator may ever set. + // + // Emissions actually skipped, so a control can assert that it DID something rather than + // that a picture changed - a control that silently never fired is the third shape of R-16's + // "a gate that cannot go red for its own reason". + // + // WHICH ONE E2'S RETRACE USES, and it is not the clear. Measured on the joint head: with + // MOBILEGL_IPC_E2_DROP_CLEAR=1 armed and its WARN in the library's own log, the OpenRA + // retrace under inproc still scored ssim=1.000000 / mismatchPixels=0 (joint-v1.md §3), + // because OpenRA covers every pixel it clears before the snapshot. Dropping the DRAWS is + // the control whose observable the golden is actually made of. See VerbPort.cpp's + // ArmControlKnobs for the trace census that settled it. + void SetDropClearEmissionForNegativeControl(Bool drop); + Uint64 DroppedClearEmissions(); + void SetDropDrawEmissionForNegativeControl(Bool drop); + Uint64 DroppedDrawEmissions(); + + // ID-47. The CLIENT refuses a readback whose answer would not fit a reply slot, BEFORE it + // emits the record, and names the read. THE REFUSAL ITSELF IS s1's - + // ClientSession::RequireReadPixelsReplyFits, forwarding to + // ReplySlotPool::RequireReadPixelsFits - and this package does not own a second copy of the + // message: two spellings of one refusal is how the two sides come to disagree about which + // reads are legal. What c1 owns is the CALL SITE and the number it passes, which is ID-49's + // tight extent; the control below is over that, and s1-v3.md §1's cases are over the + // helper. + + // ID-49. `MGPReadbackInfo::DstSize` is the TIGHT w*h*bytesPerPixel extent - the reply + // payload - and nothing about the application's pack state crosses the wire. The server + // reads with a NEUTRAL pack state into that run; this is the number both sides derive. + Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type); + + // ID-49. Scatters the tight rows into the application's pointer per the application's own + // pack state (ROW_LENGTH, SKIP_*, ALIGNMENT), which only the client holds. Exposed for the + // same reason as the refusal above: the control drives the function the emitter calls + // rather than a second copy of GL 4.6 8.4.4's arithmetic. THE GAPS ARE NEVER WRITTEN - + // they belong to the application - and that is what the control checks with a sentinel. + void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width, + GLsizei height, Uint64 bytesPerPixel, + const PixelStoreParameters& pack); + + // P7 gate 5 (g5-readback). A glReadPixels whose tight answer is larger than one reply slot + // is NOT one record: the client splits the w*h rectangle into BANDS, emits each band as an + // ordinary read_pixels record with its own box and its own tight DstSize (so every answer + // still fits one slot, ID-47, and the server's PH-3 bound - its tight answer against its own + // LinkTerms.maxReplyBytes - holds unchanged), and scatters each band into the application's destination under its + // own GL_PACK_* state. No reply is chunked; the READ is. + // + // THE SHAPE. Whole-width row bands of floor(maxReplyBytes / rowBytes) rows when a row fits + // a reply; otherwise every row is cut into column pieces of floor(maxReplyBytes / bpp) + // pixels. Both shapes are contiguous in the tight layout, which is what lets the neutral- + // pack fast path keep reading straight into the application's pointer. Bands are in + // row-major order and cover the rectangle exactly once. + // + // NO PLAN exactly when not even ONE pixel fits a reply (maxReplyBytes < bytesPerPixel, + // including a link with no reply pool at all); the emitter then refuses BY NAME through + // ID-47's unchanged helper rather than emitting anything. That is the only ReadPixels shape + // left that ends in Fatal{ReplyTooLarge}: CONTRACT-P5 row 23 makes an answer that cannot fit + // a slot a named Fatal, not a GL error, and no pixel can be returned in pieces smaller than + // itself. + // + // THE PLAN IS TWO NUMBERS AND THE BANDS ARE WALKED, NOT LISTED: a band list would be one + // allocation per read that grows with the read, and the walk below is the only place the + // order and the coverage are decided - the emitter and the unit control both call it. + struct ReadbackBand { + Uint64 FirstRow = 0; + Uint64 Rows = 0; + Uint64 FirstColumn = 0; + Uint64 Columns = 0; + }; + struct ReadbackBandPlan { + Uint64 RowsPerBand = 0; // 1 when a single row does not fit a reply + Uint64 ColumnsPerBand = 0; // the whole width when it does + }; + // False (and `plan` untouched) exactly when not even one pixel fits a reply. + Bool PlanReadbackBands(Uint64 width, Uint64 height, Uint64 bytesPerPixel, Uint64 maxReplyBytes, + ReadbackBandPlan& plan); + template + void ForEachReadbackBand(Uint64 width, Uint64 height, const ReadbackBandPlan& plan, + Visit&& visit) { + if (plan.RowsPerBand == 0 || plan.ColumnsPerBand == 0) return; + for (Uint64 row = 0; row < height; row += plan.RowsPerBand) { + const Uint64 rows = plan.RowsPerBand < height - row ? plan.RowsPerBand : height - row; + for (Uint64 column = 0; column < width; column += plan.ColumnsPerBand) { + const Uint64 columns = + plan.ColumnsPerBand < width - column ? plan.ColumnsPerBand : width - column; + visit(ReadbackBand{row, rows, column, columns}); + } + } + } + + // The scatter for ONE band: `band` holds band.Rows x band.Columns tight pixels, and they land + // where ScatterTightReadbackIntoPackState would have put those pixels of the whole + // `width`-wide read. ScatterTightReadbackIntoPackState is this function over the single band + // {0, height, 0, width}, so the whole-read arithmetic and the banded one cannot drift. + void ScatterReadbackBandIntoPackState(const void* band, void* destination, GLsizei width, + const ReadbackBand& where, Uint64 bytesPerPixel, + const PixelStoreParameters& pack); + + // ID-49. True when the destination layout IS the tight layout, which is the only condition + // under which EmitReadPixels may read the reply straight into the application pointer and + // skip the bounce. Exported because the FAST PATH and the SCATTER have to agree, and the + // only honest way to state that is to drive both and compare - a case that tested either + // alone would pass a predicate that said yes to a layout the scatter would have rearranged. + Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel, + const PixelStoreParameters& pack); + + // M2 / codex 11. True exactly when the readback reply is OK and carries the read's own exact + // extent (CONTRACT-P5 row 23). EmitReadPixels calls this and Fatals by name when it is false + // - a short OK reply, or a DECLINE/ERROR with a zero payload, is refused rather than scattered + // as pixels. Exposed so the control drives the production predicate (R-16), not a copy: pass + // 0=OK / 1=DECLINED / 2=ERROR as `status`. + Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected); + + // ============================================================================= + // P5b d1 - the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1) + // ============================================================================= + // + // The nineteen indexed / instanced / multi-draw / indirect entry points all ride draw_vbo + // (59), and the ONLY thing that differs per entry point is how the GL arguments become the + // record's fields. That derivation is split out of the emitters as three pure functions + // over a snapshot of the bindings the emitter read from the GL context, so a unit case can + // drive the PRODUCTION derivation with synthetic bindings (R-16) while the integration lane + // proves the bindings are read from the right slots. The emitters compose these and add + // nothing but the hooks, the E2 draw-drop control and the staging of a client index array. + + // What the emitter reads from the GL context before it plans a draw. + struct RemoteDrawBindings { + // The VAO's GL_ELEMENT_ARRAY_BUFFER: bound or not, and its handle when bound (the same + // handle set_index_buffer carried at validate). Not bound means `indices` is a client + // pointer and the emitter stages the bytes (kDrawHasUserIndices). + Bool ElementBufferBound = false; + MG_Pipe::MGPipeHandle ElementBuffer = MG_Pipe::kMGPipeNullHandle; + // The bound GL_DRAW_INDIRECT_BUFFER and GL_PARAMETER_BUFFER, null when unbound. + MG_Pipe::MGPipeHandle DrawIndirectBuffer = MG_Pipe::kMGPipeNullHandle; + MG_Pipe::MGPipeHandle ParameterBuffer = MG_Pipe::kMGPipeNullHandle; + // GL_PRIMITIVE_RESTART / GL_PRIMITIVE_RESTART_FIXED_INDEX and the application's index, + // carried verbatim (informational in P5b: the backend reads its own barrier-pulled copy). + Bool PrimitiveRestart = false; + Uint32 RestartIndex = 0; + // P5e (vi), ID-82 / CONTRACT-P5E §5.1: TRUE when the bound VAO has at least one ENABLED + // attribute with no buffer object behind it, i.e. an array whose vertices live in the + // application's own memory. The server has no such memory: today it dereferences + // `attrib.Offset` as a raw client pointer from the apply thread + // (Managers.cpp's SyncClientSideAttributesForDrawArrays), which is legal only while the + // client is parked behind the record. It rides in MGPDrawInfo::Flags as kDrawClientArrays + // so BOTH roles can decide from the wire - the client refuses such a draw under + // run-ahead, and MGPipeBarriered's escalation (ii) keeps it barriered if one ever + // arrives anyway. Staging the bytes is P8's. + Bool ClientVertexArrays = false; + }; + + // 1 / 2 / 4 for the three GL index types, 0 for anything else (the frontend has already + // refused those with INVALID_ENUM before the slot is reached). + Uint8 RemoteIndexSizeFor(GLenum indexType); + + // The fixed head. `indexSize` 0 = arrays. `instanceCount` is the call's own (1 for a + // non-instanced entry point) and `baseInstance` its gl_BaseInstance value; the sink reads + // "instanced" as InstanceCount != 1 || StartInstance != 0, so an instanced call with a + // count of 1 and no base instance is dispatched as the plain draw it is equivalent to. + MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, + GLuint baseInstance, Uint32 numDraws, + const RemoteDrawBindings& bindings); + + // One MGPDrawRange from one (first | indices, count, basevertex). Arrays: Start = first. + // Indexed with an element buffer bound: Start = offset / IndexSize, and FALSE when the byte + // offset is not a whole number of indices or does not fit the record's Uint32 Start - the + // caller refuses by name rather than round. Indexed with no element buffer: Start = 0 and + // the bytes are the client's (the emitter stages `count * IndexSize` of them). + Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, + GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out); + + // The kDrawIsIndirect second tail: the bound GL_DRAW_INDIRECT_BUFFER handle, the call's + // `indirect` byte offset, the stride and the draw count; for the *IndirectCount forms the + // bound GL_PARAMETER_BUFFER handle and the byte offset the call spells as `drawcount`. + MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, + GLsizei drawCount, GLsizei stride, + GLintptr parameterOffset, Bool hasParameterBuffer); + +} // namespace MobileGL::MG_Record diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp index 67d659e9..69f3d5ac 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.cpp @@ -167,8 +167,8 @@ namespace MGITest { state.cmdBytesWritten = encoder.CmdBytesWritten(); state.stageReclaimWaits = encoder.StageReclaimWaits(); state.replyPostings = session->ReplyPostings(); - state.packBufferReadbackMarks = MobileGL::MG_Remote::Client::ProducerMarkCount( - MobileGL::MG_Remote::Client::GpuWriteProducer::ReadPixelsPackBuffer); + state.packBufferReadbackMarks = MobileGL::MG_Record::ProducerMarkCount( + MobileGL::MG_Record::GpuWriteProducer::ReadPixelsPackBuffer); const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters(); state.fenceLocalAnswers = fences.LocalAnswers; state.fenceEscalations = fences.Escalations; diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index b7a73c23..c028b431 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -201,6 +201,13 @@ namespace MobileGL::MG_Pipe { } // namespace void MGPipeSetClientDeviceLostProbe(MGPipeClientDeviceLostProbe probe) { g_clientDeviceLostProbe = probe; } Bool MGPipeClientDeviceLost() { return g_clientDeviceLostProbe != nullptr && g_clientDeviceLostProbe(); } + namespace { + constinit MGPipeClientLossConfirmer g_clientLossConfirmer = nullptr; + } // namespace + void MGPipeSetClientLossConfirmer(MGPipeClientLossConfirmer confirmer) { g_clientLossConfirmer = confirmer; } + Bool MGPipeClientConfirmLossAfterDecline(Uint32 waitMs) { + return g_clientLossConfirmer != nullptr && g_clientLossConfirmer(waitMs); + } #if MOBILEGL_BUILD_RECORD_ARM // ---------------------------------------------------------------------------- diff --git a/MobileGL/MG_Pipe/PipeClientSeam.h b/MobileGL/MG_Pipe/PipeClientSeam.h index cc9b3238..1787ae64 100644 --- a/MobileGL/MG_Pipe/PipeClientSeam.h +++ b/MobileGL/MG_Pipe/PipeClientSeam.h @@ -22,4 +22,10 @@ namespace MobileGL::MG_Pipe { void MGPipeSetClientDeviceLostProbe(MGPipeClientDeviceLostProbe probe); Bool MGPipeClientDeviceLost(); + // A declined readback asks the session, briefly, whether a device loss is about to be + // reported (ClientSession::ConfirmLossAfterDecline). Without a session the answer is no. + using MGPipeClientLossConfirmer = Bool (*)(Uint32 waitMs); + void MGPipeSetClientLossConfirmer(MGPipeClientLossConfirmer confirmer); + Bool MGPipeClientConfirmLossAfterDecline(Uint32 waitMs); + } // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index f0acf261..36d1f887 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -653,6 +653,10 @@ namespace MobileGL::MG_Remote::Client { namespace { [[maybe_unused]] const bool g_deviceLostProbeRegistered = [] { MG_Pipe::MGPipeSetClientDeviceLostProbe(&ClientSession::DeviceLost); + MG_Pipe::MGPipeSetClientLossConfirmer([](Uint32 waitMs) -> Bool { + ClientSession* session = ClientSession::Active(); + return session != nullptr && session->ConfirmLossAfterDecline(waitMs); + }); return true; }(); } // namespace diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 418373c1..8214117b 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -32,10 +32,10 @@ #include "ClientSession.h" #include "GpuWritePending.h" +#include #include #include "../Server/ServerLoop.h" -#include "../Server/PipeApplier.h" #include #include @@ -76,6 +76,10 @@ namespace MobileGL::MG_Remote::Client { + // P13 W5: the pre-verb hook and the E2 evidence line are the verb port's (VerbPort.h). + using MG_Record::BeforeReadOnlyVerb; + using MG_Record::LogE2ControlLine; + // The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is // GLFunctionsTable plus Present plus SetSwapInterval; GLFunctionsTable is 69 function // pointers plus one Bool (PrefersCpuXfbPrimitiveAccounting, BackendObject.h:274). A slot @@ -88,129 +92,11 @@ namespace MobileGL::MG_Remote::Client { kRemoteEmitSlotCount * sizeof(void (*)()) + sizeof(void (*)()), "the emit table's 71 slots plus the packed Bool no longer describe the table"); - [[noreturn]] void UnmigratedVerbFatal(const char* slot) { - // The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the - // slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is - // inert in an INFO build - and INFO is what every device lane runs. - SessionFail(MGFatalFamily::UnmigratedVerb, "MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); - } - namespace { - Bool g_dropClearEmission = false; - Uint64 g_droppedClearEmissions = 0; - Bool g_dropDrawEmission = false; - Uint64 g_droppedDrawEmissions = 0; Uint64 g_presentOrdinal = 0; Uint64 g_publishedMaxRecordBytes = 0; - // E2's control has to be armable from OUTSIDE the process that runs the replay, because - // the statement it makes is about a trace lane and not about a unit case: "drop an - // emission and OpenRA's SSIM falls below 0.99". A recompile would make the control - // arm against source text, which is ID-22(a)'s defect. - // - // READ WITH getenv RATHER THAN THROUGH MG_Config, DELIBERATELY AND TEMPORARILY. Config.h - // is c0's and a new MOBILEGL_IPC_* knob goes through the integrator; these are - // NEGATIVE-CONTROL switches no operator may ever set, and they announce themselves at - // warning level every time they arm so they cannot be on by accident. Flagged for - // adoption into IpcTable if the integrator wants them there. - Bool ReadControlKnob(const char* name) { - const char* value = std::getenv(name); - return value != nullptr && value[0] == '1' && value[1] == '\0'; - } - - // ---- WHY THERE ARE TWO OF THESE KNOBS, measured rather than assumed ----------------- - // - // MOBILEGL_IPC_E2_DROP_CLEAR came first and it does exactly what it says: every glClear - // stops at the client and no Clear record reaches the ring. It STILL COULD NOT TURN THE - // E2 RETRACE RED (joint-v1.md 3: SSIM 1.000000, mismatchPixels=0 with the knob armed and - // the arming WARN in the library's own log). That is not a broken knob, it is OpenRA: - // `apitrace dump` of openra.trace over the 31249 replayed calls counts 30 glClear, 30 - // glXSwapBuffers and 788 glDrawArrays, and the final frame issues its clear at call - // 30197 and then covers the surface four times over with a terrain layer - // (`glDrawArrays(GL_TRIANGLES, first=56064, count=16128)` x4, under a scissor of - // -24,-24,688x528 over a 640x480 surface) before the snapshot at 31249. A frame that - // overdraws every pixel it clears has a picture that does not depend on the clear, so - // "drop the clear" is a control whose observable is invisible to THIS trace - R-16's - // exact defect, a gate that cannot go red for its own reason. - // - // MOBILEGL_IPC_E2_DROP_DRAW is the honest form of the same statement for a trace lane: - // drop every DrawVbo record and the picture can only be the clear colour. It is the - // control that makes "the wire carried the frame" falsifiable, because the thing it - // removes is the thing the golden is made of. - // - // DROP_CLEAR IS KEPT rather than retired: it drops a real record, it now publishes the - // count it dropped (below), and a scenario whose picture DOES depend on its clear - - // ClearThenReadPixelsScenario is the reduced path's target A - is where it is - // observable. What it is no longer allowed to be is E2's retrace control. - void ArmControlKnobs() { - g_dropClearEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_CLEAR"); - g_dropDrawEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_DRAW"); - if (g_dropClearEmission) { - MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_CLEAR=1 - a NEGATIVE CONTROL is " - "armed and every glClear will be DROPPED on the wire. It is observable " - "only where the picture depends on the clear: OpenRA overdraws its whole " - "surface every frame, so this knob does NOT redden the E2 retrace " - "(measured, joint-v1.md 3) - MOBILEGL_IPC_E2_DROP_DRAW is the one that " - "does. The dropped count is published on the 'E2 control armed' line"); - } - if (g_dropDrawEmission) { - MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_DRAW=1 - E2's NEGATIVE CONTROL is " - "armed and every DrawVbo record will be DROPPED on the wire. The surface " - "can then only carry the clear colour, so this arm is expected to fail its " - "SSIM threshold; a lane that stays green with it set is not going through " - "the wire at all"); - } - } - - // THE CONTROL'S OWN EVIDENCE LINE, and it is emitted per frame rather than at teardown - // on purpose: a retrace that is killed by its own timeout, or whose library never runs - // MobileGL::Destroy, would leave a teardown-only line absent and the control would then - // have to accept a bare threshold failure - which is the thing R-16 forbids. One line - // per Present, only while a knob is armed, is bounded by the frame count and present - // whatever happens afterwards. - void LogE2ControlLine(Uint64 frameOrdinal) { - if (!g_dropClearEmission && !g_dropDrawEmission) return; - MGLOG_W("MGPipe: E2 control armed - drop-draw=%d drop-clear=%d, %llu records dropped " - "on the wire (draw=%llu clear=%llu), frame %llu", - g_dropDrawEmission ? 1 : 0, g_dropClearEmission ? 1 : 0, - static_cast(g_droppedDrawEmissions + g_droppedClearEmissions), - static_cast(g_droppedDrawEmissions), - static_cast(g_droppedClearEmissions), - static_cast(frameOrdinal)); - } - - // ID-49's two halves, in one place so the emitter and its control read the same - // arithmetic. - // - // GL 4.6 8.4.4, pack side: the destination row stride is ROW_LENGTH (or the width) - // pixels rounded UP to PACK_ALIGNMENT, the first written byte is offset by SKIP_ROWS - // whole strides plus SKIP_PIXELS pixels, and only `width * bytesPerPixel` bytes of each - // stride are written - the gaps belong to the application and are never touched. That - // last clause is what the control checks with a sentinel. - Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel, - const PixelStoreParameters& pack) { - // SKIP_IMAGES (and IMAGE_HEIGHT) are NOT consulted: glReadPixels is a 2-D read and GL - // ignores the image-level pack parameters for it, exactly as the monolith conversion - // path does at DirectGLES.cpp:10905 (honorPackImageParams=false). A non-zero - // SkipImages therefore does not make the layout non-tight (codex 6). - if (pack.SkipRows != 0 || pack.SkipPixels != 0) return false; - if (pack.RowLength != 0 && pack.RowLength != width) return false; - const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; - const Uint64 rowBytes = static_cast(width) * bytesPerPixel; - return (rowBytes % alignment) == 0; - } - - // A band's box origin (g5-readback): the read's origin plus the band's offset, summed - // wide. It can only leave Int32 for a read whose far edge was already past INT32_MAX, - // and every pixel out there is outside any framebuffer - GL leaves their values - // undefined - so it saturates instead of wrapping onto real pixels. - Int32 ReadbackBandOrigin(GLint origin, Uint64 offset) { - const Int64 at = static_cast(origin) + static_cast(offset); - constexpr Int64 kMax = std::numeric_limits::max(); - return static_cast(at > kMax ? kMax : at); - } - // ---- the session, demanded rather than assumed -------------------------------- // // Every class-B slot needs one. A null session here is NOT the monolith answer - the @@ -233,1097 +119,45 @@ namespace MobileGL::MG_Remote::Client { return *session; } - // A verb that reads buffers but starts no shader: clear, blit, readback, present. The - // push still has to run - a coherent map is read by the GPU on any of them - but there - // is no shader that could write one, so no mark walk. - void BeforeReadOnlyVerb() { - MG_Record::PushPersistentMapsBeforeVerb(); - MG_Pipe::MGPipeDrainDeferredDestroys(); - } - - // ---- P13 W4: THE MONOLITH VERB PORT ---------------------------------------------- - // - // Monolith's record arm (MG_Config::RecordArmAliasesFrontend) reads the verb's own state - - // VerbIndirectBuffer, VerbDispatchIndirectBuffer, ... - and nothing in monolith wrote it: - // ServerVerbSink is its only writer, and monolith never decoded a verb record. So the - // ported slots run THESE emitters - the same plan, the same pre-verb hooks in the same - // order (persistent-map push, owned client-memory inputs, the GPU-write marks) - and hand - // the planned record, in this process and on this thread, to a ServerVerbSink bound to - // the one backend object, which stamps the verb state exactly as the apply thread does - // and calls the backend's REAL table (ServerVerbSink::Table, never gBackendFunctionsTable, - // so nothing re-enters this port). One planner and one sink body serve both shapes: the - // monolith record arm is the split record arm minus the codec and the ring. - Bool g_monolithVerbPort = false; - - Server::ServerVerbSink& MonolithVerbSink() { - // Leaked at exit like every other MG_Remote singleton (ID-8). - static Server::ServerVerbSink& sink = *new Server::ServerVerbSink(); - return sink; - } - - // The answer of a reply row on the port: written straight into the caller's buffer, which - // is what the session's reply slot would have been copied into. - class MonolithReplySink final : public Wire::ReplySink { + // P13 W5: the session as the record arm's VerbSession. The emitters live in MG_Record + // (MG_Impl/Pipe/Verb/VerbPort.cpp) and run without a transport too; under one they reach + // the session through this adapter, which RequireSession has just proved is there. + class ClientVerbSession final : public MG_Record::VerbSession { public: - MonolithReplySink(void* out, Uint64 capacity) : m_out(out), m_capacity(capacity) {} - void PostReply(Uint64, Int32 status, const void* bytes, Uint64 size) override { - m_posted = true; - m_status = status; - m_size = size; - if (status == kStatusOk && bytes != nullptr && m_out != nullptr && size != 0) { - std::memcpy(m_out, bytes, static_cast(std::min(size, m_capacity))); - } - } - Bool Posted() const { return m_posted; } - Int32 Status() const { return m_status; } - Uint64 Size() const { return m_size; } - - private: - void* m_out; - Uint64 m_capacity; - Bool m_posted = false; - Int32 m_status = kStatusError; - Uint64 m_size = 0; - }; - - // The port's reply cap: one band answers any read a process can hold (PlanReadbackBands - // still cuts a row wider than this, as it would for a slot). - constexpr Uint32 kMonolithMaxReplyBytes = 0x7fffffffu; - - Bool ApplyOnMonolithPort(MG_Pipe::MGPWireOp op, const void* payload, const Wire::WireTail* tails, - Uint32 tailCount, Wire::ReplySink* replies) { - Server::ServerVerbSink& sink = MonolithVerbSink(); - MG_Backend::BackendObject* backend = MG_Backend::pActiveBackendObject.get(); - if (sink.Backend() != backend) { - sink.SetBackend(backend); - sink.SetMaxReplyBytes(kMonolithMaxReplyBytes); - } - using Op = MG_Pipe::MGPWireOp; - switch (op) { - case Op::DrawVbo: { - const auto& info = *static_cast(payload); - const auto* ranges = (tailCount > 0 && tails[0].Size != 0) - ? static_cast(tails[0].Bytes) - : nullptr; - const auto* indirect = ((info.Flags & MG_Pipe::kDrawIsIndirect) != 0 && tailCount > 1) - ? static_cast(tails[1].Bytes) - : nullptr; - return sink.OnDrawVbo(info, ranges, nullptr, indirect); - } - case Op::LaunchGrid: - return sink.OnLaunchGrid(*static_cast(payload)); - // ---- W4b: the texture family's verbs ---- - case Op::GenerateMipmap: - return sink.OnGenerateMipmap(*static_cast(payload)); - case Op::CopyFramebufferToTexture: - return sink.OnCopyFramebufferToTexture(*static_cast(payload)); - case Op::ResourceCopyRegion: - return sink.OnResourceCopyRegion(*static_cast(payload)); - // ---- W4b: the readbacks. The reply rows answer through `replies`, which writes the - // caller's buffer; the pack-buffer rows land in the buffer through the applier. - case Op::ReadPixels: - return sink.OnReadPixels(*static_cast(payload), 0, replies); - case Op::GetTextureImage: - return sink.OnGetTextureImage(*static_cast(payload), 0, replies); - case Op::ReadPixelsToBuffer: - return sink.OnReadPixelsToBuffer(*static_cast(payload)); - case Op::GetTextureImageToBuffer: - return sink.OnGetTextureImageToBuffer(*static_cast(payload)); - // ---- W4c: framebuffers, image units, storage-block bindings ---- - case Op::Clear: - return sink.OnClear(*static_cast(payload)); - case Op::Blit: - return sink.OnBlit(*static_cast(payload)); - case Op::BindShaderImage: - return sink.OnBindShaderImage(*static_cast(payload)); - // ---- W4a's XFB half: the capture span and its object, by lifetime id ---- - case Op::BeginStreamOutput: - return sink.OnBeginStreamOutput(*static_cast(payload)); - case Op::EndStreamOutput: - return sink.OnEndStreamOutput(*static_cast(payload)); - case Op::PauseStreamOutput: - return sink.OnPauseStreamOutput(*static_cast(payload)); - case Op::ResumeStreamOutput: - return sink.OnResumeStreamOutput(*static_cast(payload)); - case Op::BindStreamOutput: - return sink.OnBindStreamOutput(*static_cast(payload)); - case Op::DeleteStreamOutput: - return sink.OnDeleteStreamOutput(*static_cast(payload)); - case Op::SetStorageBlockBinding: { - // The name is the emitter's own NUL-terminated string (VerbChannel::StageBytes on - // the port names it by address), alive for this call. - const auto& record = *static_cast(payload); - const auto* name = reinterpret_cast(static_cast(record.Name.Offset)); - return sink.OnSetStorageBlockBinding(record, name); - } - default: - // A slot InstallMonolithVerbPort routes here whose row this switch does not know: - // the two lists drifted, and falling through to the driver would be the very - // "ran the other arm and looked fine" this port exists to end. - UnmigratedVerbFatal("MonolithVerbPort"); - } - } - - // WHERE A PORTED EMITTER'S RECORD GOES: the session's ring, or - on monolith's record arm - - // the in-process sink above. Spelled like the session (EmitAndWait / EmitAndWaitTails / - // MaxReplyBytes / ...) so an emitter reads the same either way. - class VerbChannel { - public: - explicit VerbChannel(ClientSession* session) : m_session(session) {} - - Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, - const void* varTail, Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, - Int32* statusOut, Uint64* replySizeOut = nullptr) { - if (m_session != nullptr) { - return m_session->EmitAndWait(op, payload, payloadBytes, varTail, varTailBytes, replyOut, - replyBytes, statusOut, replySizeOut); - } - const Wire::WireTail tail{varTail, varTailBytes}; - Port(op, payload, &tail, varTail != nullptr ? 1u : 0u, replyOut, replyBytes, statusOut, replySizeOut); - return 0; + Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, const void* varTail, + Uint64 varTailBytes, void* replyOut, Uint64 replyBytes, Int32* statusOut, + Uint64* replySizeOut) override { + return ClientSession::Active()->EmitAndWait(op, payload, payloadBytes, varTail, varTailBytes, replyOut, + replyBytes, statusOut, replySizeOut); } Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes, - const Wire::WireTail* tails, Uint32 tailCount, void* replyOut, Uint64 replyBytes, - Int32* statusOut, Uint64* replySizeOut = nullptr) { - if (m_session != nullptr) { - return m_session->EmitAndWaitTails(op, payload, payloadBytes, tails, tailCount, replyOut, - replyBytes, statusOut, replySizeOut); - } - Port(op, payload, tails, tailCount, replyOut, replyBytes, statusOut, replySizeOut); - return 0; - } - Uint32 MaxReplyBytes() const { - return m_session != nullptr ? m_session->MaxReplyBytes() : kMonolithMaxReplyBytes; + const MG_Pipe::MGPipeVerbTail* tails, Uint32 tailCount, void* replyOut, + Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) override { + return ClientSession::Active()->EmitAndWaitTails(op, payload, payloadBytes, tails, tailCount, replyOut, + replyBytes, statusOut, replySizeOut); } + Uint32 MaxReplyBytes() const override { return ClientSession::Active()->MaxReplyBytes(); } void RequireReadPixelsReplyFits(Uint32 width, Uint32 height, Uint32 format, Uint32 type, - Uint64 bytes) const { - if (m_session != nullptr) m_session->RequireReadPixelsReplyFits(width, height, format, type, bytes); + Uint64 bytes) const override { + ClientSession::Active()->RequireReadPixelsReplyFits(width, height, format, type, bytes); } - // A blob the record names by reference: staged in SEG_STAGE under a session, its own - // address on the port (kMGHostSpanSegNone, the monolith spelling MGPipeHostBytes reads). - MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size) { - if (m_session != nullptr) return m_session->Encoder().StageBytes(bytes, size); - return MG_Pipe::MGPBlobRef{static_cast(reinterpret_cast(bytes)), size, - MG_Pipe::kMGHostSpanSegNone, 0}; + MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size) override { + return ClientSession::Active()->Encoder().StageBytes(bytes, size); } - - private: - void Port(MG_Pipe::MGPWireOp op, const void* payload, const Wire::WireTail* tails, Uint32 tailCount, - void* replyOut, Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut) { - MonolithReplySink replies(replyOut, replyBytes); - const Bool applied = ApplyOnMonolithPort(op, payload, tails, tailCount, &replies); - // A sink that declined without answering is the decoder's DECLINED; an answer it - // posted is the answer, whatever the return. - const Int32 status = replies.Posted() - ? replies.Status() - : (applied ? Wire::ReplySink::kStatusOk : Wire::ReplySink::kStatusDeclined); - if (statusOut != nullptr) *statusOut = status; - if (replySizeOut != nullptr) *replySizeOut = replies.Posted() ? replies.Size() : 0; - if (!applied && replyOut == nullptr) { - MGLOG_E_ONCE("MGPipe: the monolith verb port's op %u was declined by the sink; nothing was " - "drawn, dispatched or changed", - static_cast(op)); - } - } - - ClientSession* m_session; }; - // The channel a ported emitter uses: the port when monolith's record arm installed it - // (only the ported slots reach an emitter there), the active session otherwise. - VerbChannel ChannelFor(const char* slot) { - if (g_monolithVerbPort) return VerbChannel(nullptr); - return VerbChannel(&RequireSession(slot)); + MG_Record::VerbSession& ResolveVerbSession(const char* slot) { + (void)RequireSession(slot); + static ClientVerbSession adapter; + return adapter; } - // ============================================================================= - // CLASS B - the five slots the verb census measured (CONTRACT-P5.md §7) - // ============================================================================= - - void EmitClear(GLbitfield mask) { - VerbChannel session = ChannelFor("Clear"); - BeforeReadOnlyVerb(); - - if (g_dropClearEmission) { - // A negative control. Everything above still ran, so the only difference - // between this arm and the live one is the record - which is exactly the - // statement "the picture comes from the wire" that E2 exists to prove. Its - // OBSERVABILITY is a property of the workload, not of this branch: see - // ArmControlKnobs for the measurement that took E2's retrace off this knob. - ++g_droppedClearEmissions; - return; - } - - MG_Pipe::MGPClear record{}; - // The DRAW framebuffer is whatever the server's own SyncRenderState resolves from - // gPipeInputs, which the client's MGP_FILL(Clear) at GL_Drawing.cpp:534 has just - // written and the verb barrier keeps still (R-1). Naming a handle here would be a - // SECOND statement of the binding, and the second one is the one that goes stale. - record.Fbo = MG_Pipe::kMGPipeNullHandle; - record.Kind = kRemoteClearWhole; - record.DrawBufferIndex = -1; - record.BufferMask = static_cast(mask); - record.ValueClass = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), nullptr, 0, - nullptr, 0, nullptr); - } - - - // ---- f1: verbatim clear/copy/mipmap records (CONTRACT-P5B §2) ---- - void EmitF1Clear(const char* slot, MG_Pipe::MGPipeHandle fbo, GLenum buffer, - GLint drawbuffer, Uint8 valueClass, const void* value, - GLfloat depth = 0, GLint stencil = 0) { - VerbChannel session = ChannelFor(slot); - BeforeReadOnlyVerb(); - MG_Pipe::MGPClear record{}; - record.Fbo = fbo; - record.DrawBufferIndex = drawbuffer; - record.ValueClass = valueClass; - switch (buffer) { - case GL_COLOR: - record.Kind = MG_Pipe::kMGPipeClearKindColor; - std::memcpy(record.ColorValue, value, sizeof(record.ColorValue)); - break; - case GL_DEPTH: - record.Kind = MG_Pipe::kMGPipeClearKindDepth; - std::memcpy(&record.DepthValue, value, sizeof(record.DepthValue)); - break; - case GL_STENCIL: - record.Kind = MG_Pipe::kMGPipeClearKindStencil; - std::memcpy(&record.StencilValue, value, sizeof(record.StencilValue)); - break; - case GL_DEPTH_STENCIL: - record.Kind = MG_Pipe::kMGPipeClearKindDepthStencil; - record.DepthValue = depth; - record.StencilValue = stencil; - break; - default: UnmigratedVerbFatal(slot); - } - session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { - EmitF1Clear("ClearBufferfv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, - MG_Pipe::kMGPipeClearValueClassFloat, value); - } - void EmitClearNamedFramebufferfv(const SharedPtr& fbo, - GLenum buffer, GLint drawbuffer, const GLfloat* value) { - EmitF1Clear("ClearNamedFramebufferfv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), - buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value); - } - - void EmitClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { - EmitF1Clear("ClearBufferiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, - MG_Pipe::kMGPipeClearValueClassInt, value); - } - void EmitClearNamedFramebufferiv(const SharedPtr& fbo, - GLenum buffer, GLint drawbuffer, const GLint* value) { - EmitF1Clear("ClearNamedFramebufferiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), - buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value); - } - - void EmitClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { - EmitF1Clear("ClearBufferuiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, - MG_Pipe::kMGPipeClearValueClassUint, value); - } - void EmitClearNamedFramebufferuiv(const SharedPtr& fbo, - GLenum buffer, GLint drawbuffer, const GLuint* value) { - EmitF1Clear("ClearNamedFramebufferuiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), - buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value); - } - - void EmitClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { - EmitF1Clear("ClearBufferfi", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, - MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); - } - void EmitClearNamedFramebufferfi(const SharedPtr& fbo, - GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { - EmitF1Clear("ClearNamedFramebufferfi", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), - buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); - } - const SharedPtr& F1BoundTexture(GLenum target) { - auto& ctx = *MG_State::pGLContext; - return ctx.GetTextureUnitObject(ctx.GetActiveTextureUnit()) - .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject(); - } - void EmitF1Copy(GLenum target, GLint level, GLenum format, GLint x, GLint y, - GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, Bool subImage) { - VerbChannel session = ChannelFor(subImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); - BeforeReadOnlyVerb(); - MG_Pipe::MGPCopyFromFramebuffer record{}; - record.Dst = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*F1BoundTexture(target)); - record.Target = static_cast(target); - record.Level = level; - record.InternalFormat = format; - record.X = x; record.Y = y; - record.Width = width; record.Height = height; - record.XOffset = xoffset; record.YOffset = yoffset; - record.SubImage = subImage; - session.EmitAndWait(MG_Pipe::MGPWireOp::CopyFramebufferToTexture, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - void EmitCopyTexImage2D(GLenum target, GLint level, GLenum format, GLint x, GLint y, - GLsizei width, GLsizei height, GLint) { - EmitF1Copy(target, level, format, x, y, width, height, 0, 0, false); - } - void EmitCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, - GLint x, GLint y, GLsizei width, GLsizei height) { - EmitF1Copy(target, level, 0, x, y, width, height, xoffset, yoffset, true); - } - void EmitGenerateMipmap(GLenum target) { - VerbChannel session = ChannelFor("GenerateMipmap"); - BeforeReadOnlyVerb(); - const auto& texture = F1BoundTexture(target); - MG_Pipe::MGPMipPlan record{}; - record.Res = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*texture); - record.Target = static_cast(target); - record.BaseLevel = texture->GetLevelRange().x(); - const auto* mipmap = dynamic_cast(texture.get()); - if (mipmap && !texture->GetUploadTargets().empty()) { - const auto plan = MG_Impl::GLImpl::ComputeMipmapGenerationRange(*mipmap, texture->GetUploadTargets()[0]); - // LevelCount is the logical end-exclusive, not the number of - // levels following BaseLevel. Preserve that existing carrier. - record.LevelCount = static_cast(std::min(plan.End, mipmap->GetMipmapLevelCount())); - } - session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - // All draw entry points share the owned-input preparation and emission below. - [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier); - void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, - const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, - const void* clientIndices, Uint64 clientIndexBytes, - const MG_Pipe::MGPDrawIndirect* indirect); - - // Does the bound VAO fetch any ENABLED attribute out of the application's own memory. - // The test is EmitVertexBuffers' own (`attrib.Enabled && !attrib.Buffer` is exactly what - // it publishes as Res == kMGPipeNullHandle), so the flag on the wire and the record the - // server applies agree by construction. - Bool BoundVaoHasClientVertexArrays(MG_State::GLState::GLContext* ctx) { - if (ctx == nullptr) return false; - const auto& vao = ctx->GetBoundVertexArray(); - if (!vao) return false; - const auto& attributes = vao->GetAllAttributes(); - for (SizeT i = 0; i < attributes.size(); ++i) { - if (attributes[i].Enabled && !attributes[i].Buffer) return true; - } - return false; - } - - void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) { - const Bool clientArrays = BoundVaoHasClientVertexArrays(MG_State::pGLContext.get()); - - MG_Pipe::MGPDrawInfo info{}; - info.Mode = static_cast(mode); - info.IndexSize = 0; // arrays - // NO kDrawHasUserIndices: the reduced path draws from a VBO. kDrawClientArrays IS - // set when one is present, because the wait rule is computed from the record on - // both sides and this path publishes its own record rather than PlanDrawInfo's. - info.Flags = clientArrays ? static_cast(MG_Pipe::kDrawClientArrays) : 0; - info.InstanceCount = 1; - info.StartInstance = 0; - info.RestartIndex = 0; - info.DrawIdOffset = 0; - info.IndexResource = MG_Pipe::kMGPipeNullHandle; - info.MinIndex = ~0u; // "unknown", MGPipeTypes.h:1330 - info.MaxIndex = ~0u; - info.XfbCpuCapturedVertices = 0; - info.NumDraws = 1; - - const MG_Pipe::MGPDrawRange range{static_cast(first), static_cast(count), 0}; - // One tail of exactly NumDraws entries. w1's encoder recomputes that from the - // payload and Fatals on a disagreement, on THIS side - so a NumDraws that drifted - // from the tail is a producer-side abort rather than a corrupt stream a peer has to - // diagnose. - EmitDrawRecord("DrawArrays", info, &range, 1, nullptr, 0, nullptr); - } - - // ============================================================================= - // P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots - // (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL - // call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink - // reproduces the backend call the monolith makes. - // ============================================================================= - // - // Every entry point below is: read the bindings, plan the head and the ranges (the pure - // functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb - // hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record; - // ID-18), honours the E2 draw-drop control for every draw record and not only the P5 - // one, stages a client index array into SEG_STAGE when there is one, and emits. - // - // Client arrays and client indices become owned ordinary buffer resources. - // Remaining invalid/unrepresentable draw shapes are named before encoding: - // +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect` - // would be a host pointer, which rule B forbids on the wire. - // +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has - // already raised INVALID_OPERATION for it; stated so the emitter - // cannot send a null handle where the sink dereferences one). - // +INDEX_OFFSET an element-buffer byte offset that is not a whole number of - // indices (or past 2^32 of them): the record spells Start in - // indices, and rounding would draw from the wrong element. - - [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) { - char name[96]; - std::snprintf(name, sizeof(name), "%s+%s", slot, qualifier); - UnmigratedVerbFatal(name); - } - - // The bindings the plan is made from, read ONCE per draw from the frontend context on - // the GL thread. The element buffer is the VAO's (the same slot EmitIndexBuffer read at - // validate), the two indirect buffers are the context's, and the handles are LOOKED UP, - // never minted: a push build mints in the BufferObject constructor. - RemoteDrawBindings ReadDrawBindings() { - RemoteDrawBindings b{}; - MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get(); - if (ctx == nullptr) return b; - if (const auto& vao = ctx->GetBoundVertexArray()) { - if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) { - b.ElementBufferBound = true; - b.ElementBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*bound); - } - // P5e (vi): the client-array probe, in the SAME single read of the bindings the - // rest of the plan is made from rather than in a second walk at the refusal - - // this runs on the GL thread once per draw and the refusal must not add its own - // frontend pass. The test is the emitter's own: EmitVertexBuffers publishes - // Res == kMGPipeNullHandle for exactly `attrib.Enabled && !attrib.Buffer`, so - // the flag and the record agree by construction instead of by inspection. - const auto& attributes = vao->GetAllAttributes(); - for (SizeT i = 0; i < attributes.size(); ++i) { - if (attributes[i].Enabled && !attributes[i].Buffer) { - b.ClientVertexArrays = true; - break; - } - } - } - if (const auto& di = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::DrawIndirect).GetBoundObject()) { - b.DrawIndirectBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*di); - } - if (const auto& pb = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::Parameter).GetBoundObject()) { - b.ParameterBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*pb); - } - b.PrimitiveRestart = ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || - ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); - b.RestartIndex = ctx->GetPrimitiveRestartIndex(); - return b; - } - - // The one emission for all nineteen. `clientIndices`/`clientIndexBytes` name a client - // index array to stage (no element buffer bound); `indirect` is the kDrawIsIndirect - // block. The two are exclusive by construction here and by the layout on both sides. - void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, - const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, - const void* clientIndices, Uint64 clientIndexBytes, - const MG_Pipe::MGPDrawIndirect* indirect) { - VerbChannel session = ChannelFor(slot); - MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); - MG_Pipe::MGPipeDrainDeferredDestroys(); - // Snapshot before marking THIS draw's potential GPU writes: resolving - // a GPU-produced EBO here must not clear its pending mark for this draw. - UniquePtr ownedInputs; - if ((info.Flags & MG_Pipe::kDrawClientArrays) != 0 || clientIndexBytes != 0) { - if (MG_State::pGLContext == nullptr) RefuseDrawByName(slot, "NO_CONTEXT"); - ownedInputs = MakeUnique(*MG_State::pGLContext); - if (!ownedInputs->Prepare(info, ranges, numDraws, clientIndices, clientIndexBytes, indirect)) { - MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation, - MakeUnique("MG_Remote/Client", slot, - "the client vertex/index fetch range cannot be represented by its storage")); - return; - } - } - MarkGpuWritesForDraw(); - - if (g_dropDrawEmission) { - // E2's negative control covers EVERY draw record, not only DrawArrays: the - // Minecraft traces are DrawElements frames, and a control that dropped only the - // one P5 entry point would leave those lanes green with the wire disarmed. - ++g_droppedDrawEmissions; - return; - } - - info.NumDraws = numDraws; - Wire::WireTail tails[2] = {{ranges, static_cast(numDraws) * sizeof(MG_Pipe::MGPDrawRange)}, - {nullptr, 0}}; - Uint32 tailCount = 1; - if (indirect != nullptr) { - info.Flags |= MG_Pipe::kDrawIsIndirect; - tails[1] = {indirect, sizeof(*indirect)}; - tailCount = 2; - } - session.EmitAndWaitTails(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails, - tailCount, nullptr, 0, nullptr); - } - - // ---- the single-draw indexed family: DrawElements, DrawElementsBaseVertex, the two - // DrawRangeElements*, the four DrawElementsInstanced* ------------------------------- - void EmitIndexedDraw(const char* slot, GLenum mode, GLsizei count, GLenum type, - const void* indices, GLint baseVertex, GLsizei instanceCount, - GLuint baseInstance, Bool hasRange, GLuint start, GLuint end) { - const RemoteDrawBindings bindings = ReadDrawBindings(); - const Uint8 indexSize = RemoteIndexSizeFor(type); - if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); - MG_Pipe::MGPDrawInfo info = - PlanDrawInfo(mode, indexSize, instanceCount, baseInstance, 1, bindings); - if (hasRange) { - info.Flags |= MG_Pipe::kDrawHasIndexRange; - info.MinIndex = start; - info.MaxIndex = end; - } - MG_Pipe::MGPDrawRange range{}; - if (!PlanDrawRange(bindings, indexSize, indices, count, baseVertex, range)) { - RefuseDrawByName(slot, "INDEX_OFFSET"); - } - // No element buffer: `indices` is the application's array and the draw's own - // range is exactly what the driver would read. A null pointer or an empty draw - // stages nothing and crosses as a zero-length range, which is what the monolith - // hands the driver too. - const Bool clientArray = !bindings.ElementBufferBound && indices != nullptr && count > 0; - EmitDrawRecord(slot, info, &range, 1, clientArray ? indices : nullptr, - clientArray ? static_cast(count) * indexSize : 0, nullptr); - } - - void EmitDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { - EmitIndexedDraw("DrawElements", mode, count, type, indices, 0, 1, 0, false, 0, 0); - } - void EmitDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, - GLint basevertex) { - EmitIndexedDraw("DrawElementsBaseVertex", mode, count, type, indices, basevertex, 1, 0, - false, 0, 0); - } - void EmitDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, - const void* indices) { - EmitIndexedDraw("DrawRangeElements", mode, count, type, indices, 0, 1, 0, true, start, end); - } - void EmitDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, - GLenum type, const void* indices, GLint basevertex) { - EmitIndexedDraw("DrawRangeElementsBaseVertex", mode, count, type, indices, basevertex, 1, - 0, true, start, end); - } - void EmitDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, - GLsizei instancecount) { - EmitIndexedDraw("DrawElementsInstanced", mode, count, type, indices, 0, instancecount, 0, - false, 0, 0); - } - void EmitDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, - const void* indices, GLsizei instancecount, - GLint basevertex) { - EmitIndexedDraw("DrawElementsInstancedBaseVertex", mode, count, type, indices, basevertex, - instancecount, 0, false, 0, 0); - } - void EmitDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, - const void* indices, GLsizei instancecount, - GLuint baseinstance) { - EmitIndexedDraw("DrawElementsInstancedBaseInstance", mode, count, type, indices, 0, - instancecount, baseinstance, false, 0, 0); - } - void EmitDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, - const void* indices, GLsizei instancecount, - GLint basevertex, GLuint baseinstance) { - EmitIndexedDraw("DrawElementsInstancedBaseVertexBaseInstance", mode, count, type, indices, - basevertex, instancecount, baseinstance, false, 0, 0); - } - - // ---- the instanced array draws ------------------------------------------------ - void EmitArraysDraw(const char* slot, GLenum mode, GLint first, GLsizei count, - GLsizei instanceCount, GLuint baseInstance) { - const RemoteDrawBindings bindings = ReadDrawBindings(); - MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, instanceCount, baseInstance, 1, bindings); - MG_Pipe::MGPDrawRange range{}; - PlanDrawRange(bindings, 0, reinterpret_cast(static_cast(first)), - count, 0, range); - EmitDrawRecord(slot, info, &range, 1, nullptr, 0, nullptr); - } - void EmitDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { - EmitArraysDraw("DrawArraysInstanced", mode, first, count, instancecount, 0); - } - void EmitDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, - GLsizei instancecount, GLuint baseinstance) { - // The vertex-FETCH base instance already crossed in set_vertex_buffers::BaseInstance - // at validate (D-H1: MGP_SET_BASE_INSTANCE runs before MGP_FILL); StartInstance is - // gl_BaseInstance's value and feeds the GL call. - EmitArraysDraw("DrawArraysInstancedBaseInstance", mode, first, count, instancecount, - baseinstance); - } - - // ---- the multi-draws: MGPDrawRange[drawcount] is exactly their shape ------------- - // - // The range array is built in a scratch vector owned by the GL thread (the emitter is - // single-threaded by the ring's own SPSC contract), sized by drawcount, never held past - // the emission. A drawcount of 0 is a call that draws nothing and emits nothing: the - // frontend has already refused a negative one. - Vector& MultiDrawScratch(Uint32 count) { - static Vector& scratch = *new Vector(); - scratch.resize(count); - return scratch; - } - - void EmitMultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { - if (drawcount <= 0 || first == nullptr || count == nullptr) return; - const RemoteDrawBindings bindings = ReadDrawBindings(); - const auto n = static_cast(drawcount); - MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, 1, 0, n, bindings); - Vector& ranges = MultiDrawScratch(n); - for (Uint32 i = 0; i < n; ++i) { - PlanDrawRange(bindings, 0, - reinterpret_cast(static_cast(first[i])), - count[i], 0, ranges[i]); - } - EmitDrawRecord("MultiDrawArrays", info, ranges.data(), n, nullptr, 0, nullptr); - } - - void EmitMultiIndexedDraw(const char* slot, GLenum mode, const GLsizei* count, GLenum type, - const GLvoid* const* indices, GLsizei drawcount, - const GLint* basevertex) { - if (drawcount <= 0 || count == nullptr || indices == nullptr) return; - const RemoteDrawBindings bindings = ReadDrawBindings(); - const Uint8 indexSize = RemoteIndexSizeFor(type); - if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); - const auto n = static_cast(drawcount); - MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, n, bindings); - Vector& ranges = MultiDrawScratch(n); - Vector ownedIndices; - for (Uint32 i = 0; i < n; ++i) { - if (!PlanDrawRange(bindings, indexSize, indices[i], count[i], - basevertex != nullptr ? basevertex[i] : 0, ranges[i])) { - RefuseDrawByName(slot, "INDEX_OFFSET"); - } - if (!bindings.ElementBufferBound) { - const Uint64 byteCount = static_cast(ranges[i].Count) * indexSize; - if (byteCount > std::numeric_limits::max() - ownedIndices.size() || - (byteCount != 0 && indices[i] == nullptr)) { - MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation, - MakeUnique("MG_Remote/Client", slot, "invalid client index span")); - return; - } - ranges[i].Start = static_cast(ownedIndices.size() / indexSize); - const SizeT at = ownedIndices.size(); - ownedIndices.resize(at + static_cast(byteCount)); - if (byteCount != 0) std::memcpy(ownedIndices.data() + at, indices[i], byteCount); - } - } - EmitDrawRecord(slot, info, ranges.data(), n, ownedIndices.empty() ? nullptr : ownedIndices.data(), - ownedIndices.size(), nullptr); - } - void EmitMultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, - const GLvoid* const* indices, GLsizei drawcount) { - EmitMultiIndexedDraw("MultiDrawElements", mode, count, type, indices, drawcount, nullptr); - } - void EmitMultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, - const GLvoid* const* indices, GLsizei drawcount, - const GLint* basevertex) { - EmitMultiIndexedDraw("MultiDrawElementsBaseVertex", mode, count, type, indices, drawcount, - basevertex); - } - - // ---- the indirect family: the block is the second tail, NumDraws is 0 ------------- - void EmitIndirectDraw(const char* slot, GLenum mode, GLenum type, const void* indirect, - GLsizei drawcount, GLsizei stride, GLintptr parameterOffset, - Bool hasParameterBuffer) { - const RemoteDrawBindings bindings = ReadDrawBindings(); - const Uint8 indexSize = type != 0 ? RemoteIndexSizeFor(type) : 0; - if (type != 0 && indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); - if (MG_Pipe::MGPipeHandleIsNull(bindings.DrawIndirectBuffer)) { - RefuseDrawByName(slot, "CLIENT_COMMANDS"); - } - if (hasParameterBuffer && MG_Pipe::MGPipeHandleIsNull(bindings.ParameterBuffer)) { - RefuseDrawByName(slot, "UNBOUND_PARAMETER"); - } - MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, 0, bindings); - const MG_Pipe::MGPDrawIndirect block = PlanDrawIndirect(bindings, indirect, drawcount, stride, - parameterOffset, hasParameterBuffer); - EmitDrawRecord(slot, info, nullptr, 0, nullptr, 0, &block); - } - void EmitDrawArraysIndirect(GLenum mode, const void* indirect) { - EmitIndirectDraw("DrawArraysIndirect", mode, 0, indirect, 1, 0, 0, false); - } - void EmitDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { - EmitIndirectDraw("DrawElementsIndirect", mode, type, indirect, 1, 0, 0, false); - } - void EmitMultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, - GLsizei stride) { - EmitIndirectDraw("MultiDrawArraysIndirect", mode, 0, indirect, drawcount, stride, 0, false); - } - void EmitMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, - GLsizei drawcount, GLsizei stride) { - EmitIndirectDraw("MultiDrawElementsIndirect", mode, type, indirect, drawcount, stride, 0, - false); - } - void EmitMultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, - GLsizei maxdrawcount, GLsizei stride) { - EmitIndirectDraw("MultiDrawArraysIndirectCount", mode, 0, indirect, maxdrawcount, stride, - drawcount, true); - } - void EmitMultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, - GLintptr drawcount, GLsizei maxdrawcount, - GLsizei stride) { - EmitIndirectDraw("MultiDrawElementsIndirectCount", mode, type, indirect, maxdrawcount, - stride, drawcount, true); - } - - void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, - GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, - GLenum filter) { - VerbChannel session = ChannelFor("BlitFramebuffer"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPBlit record{}; - // Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set - // by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the - // barrier. The named form below carries both handles after a scoped binding override. - record.ReadFbo = MG_Pipe::kMGPipeNullHandle; - record.DrawFbo = MG_Pipe::kMGPipeNullHandle; - record.SrcX0 = srcX0; - record.SrcY0 = srcY0; - record.SrcX1 = srcX1; - record.SrcY1 = srcY1; - record.DstX0 = dstX0; - record.DstY0 = dstY0; - record.DstX1 = dstX1; - record.DstY1 = dstY1; - record.Mask = static_cast(mask); - record.Filter = static_cast(filter); - session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, - nullptr, 0, nullptr); - } - - void EmitBlitNamedFramebuffer( - const SharedPtr& read, - const SharedPtr& draw, - GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, - GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { - VerbChannel session = ChannelFor("BlitNamedFramebuffer"); - // P5c (hd, CONTRACT-P5C §3.3): the scoped rebind of the client's own read/draw - // binding slots is DELETED. It existed only to stage values for the server's - // binding-slot read, and that read is gone: the sink resolves both framebuffers - // from the record's handles. The validate still refreshes both framebuffers' - // emitted state (their Named set_framebuffer_state records), which is now the - // only description the server syncs from. No driver call or binding change - // happens on the client. - MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer); - BeforeReadOnlyVerb(); - MG_Pipe::MGPBlit record{}; - record.ReadFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*read); - record.DrawFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*draw); - record.SrcX0 = srcX0; record.SrcY0 = srcY0; - record.SrcX1 = srcX1; record.SrcY1 = srcY1; - record.DstX0 = dstX0; record.DstY0 = dstY0; - record.DstX1 = dstX1; record.DstY1 = dstY1; - record.Mask = static_cast(mask); - record.Filter = static_cast(filter); - session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, - nullptr, 0, nullptr); - } - - // How many bytes glReadPixels will pack for this rectangle, from the PACK half of the - // pixel-store state. - // - // ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT. - // The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips, - // alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and - // the real backend honours the live pack state, so a 4x3 RGBA8 read with - // PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote - // to byte 120. That is the shape of the joint inproc lane's two - // DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends. - // - // So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack - // state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT - - // which is the side that holds the application's pack state, and the only side that - // can - scatters those rows into the application's pointer. "OnReadPixels writes - // exactly DstSize bytes" still holds; DstSize is now a number both sides derive from - // the same three values instead of one side deriving it from state the other cannot - // see. - // - // IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split - // entries touch take the same rule with no special case, because the rule is about the - // LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to. - Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) { - const TextureInputFormat inputFormat = - MG_Util::ConvertGLEnumToTextureInputFormat(format); - const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); - const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(inputFormat, dataType); - if (bytesPerPixel == 0) { - // NOT a guess and not a zero-length reply. A format this build cannot size is a - // readback whose answer would be silently truncated, which is the one failure a - // picture comparison cannot see. - SessionFail(MGFatalFamily::UnsizedReadback, "MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x " - "- the client must declare MGPReadbackInfo::DstSize and cannot size this " - "pair; P5's reduced path reads RGBA/UNSIGNED_BYTE", - static_cast(format), static_cast(type)); - } - return static_cast(bytesPerPixel); - } - - Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) { - if (width <= 0 || height <= 0) return 0; - return static_cast(width) * static_cast(height) * - ReadbackBytesPerPixel(format, type); - } - - // GL_PACK_SWAP_BYTES as the width of the byte group it reverses for `type`, 0 when there - // is nothing to reverse. One rule for both places a swap happens: the reply form swaps - // here, and the pack-buffer form (P9) carries the number to the server, which swaps there. - Uint32 ReadbackSwapGroup(GLenum type, const PixelStoreParameters& pack) { - if (!pack.SwapBytes) return 0; - const auto dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); - SizeT group = MG_Util::GetSizedTexturePixelDataTypeSize(dataType); - if (group == 0) group = MG_Util::GetBaseTexturePixelDataTypeSize(dataType); - // This packed depth/stencil type contains two independent 32-bit words. - if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) group = 4; - return group <= 1 ? 0 : static_cast(group); - } - - void ApplyReadbackByteSwap(void* pixels, Uint64 bytes, GLenum type, - const PixelStoreParameters& pack) { - const SizeT group = ReadbackSwapGroup(type, pack); - if (group == 0 || pixels == nullptr) return; - auto* data = static_cast(pixels); - for (Uint64 at = 0; at + group <= bytes; at += group) { - for (SizeT i = 0; i < group / 2; ++i) { - std::swap(data[at + i], data[at + group - 1 - i]); - } - } - } - - // M2 / codex 11: the reply the server posted is COMPLETE and OK. A short OK reply, and a - // DECLINED or ERROR reply with a zero payload, both leave the destination full of stale - // bytes; scattering or returning it is the silently truncated picture ID-47's own comment - // says an SSIM comparison cannot see, arriving through the status field rather than - // through truncation. `expected` is the record's own DstSize (CONTRACT-P5 row 23: the - // reply's exact extent). The predicate is at namespace scope (below) and exposed for the - // control, so R-16's "drive the production predicate" holds rather than a second copy of - // the rule in the test. - - // How long a declined readback waits for the control plane or the bell to say "lost" - // before it is judged a refusal. The SessionFault is sent before the decline is posted, - // so this is only ever spent on a server that refused for real. - constexpr Uint32 kDeclineVerdictMs = 1000; - - // The same predicate at the call site, with the named Fatal each failure mode owns. - // status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on - // an oversize reply, so the only failures left are a wrong status or a SHORT one. - // - // A LOST DEVICE IS NOT ONE OF THOSE FAILURES. Once the session is device-lost every verb - // is declined by EmitAndWait itself, and the GL answer for a lost context is that its - // commands do nothing: the read writes no pixel and the application learns of the loss - // from glGetGraphicsResetStatus. Dying here instead turned a server-side GPU fault into a - // crash of the client (Chrome's in-process GPU thread took the browser with it). False: - // the caller leaves the destination untouched and returns. - Bool RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) { - if (status != Wire::ReplySink::kStatusOk && ClientSession::DeviceLost()) return false; - // THE DECLINE CAN OUTRUN THE NEWS. A server whose device was lost inside THIS readback - // latches the session - publishing its SessionFault on the control plane - and then - // declines the record; the client can read the declined answer before it has looked at - // the control plane or seen the hangup that follows. So a decline asks once more, - // briefly, before it is taken for a server without GL.ReadPixels. - if (status == Wire::ReplySink::kStatusDeclined) { - if (ClientSession* session = ClientSession::Active(); - session != nullptr && session->ConfirmLossAfterDecline(kDeclineVerdictMs)) { - return false; - } - } - if (status == Wire::ReplySink::kStatusError) { - SessionFail(MGFatalFamily::ReplyError, "MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; " - "the destination is left untouched rather than filled with stale bytes"); - } - if (status == Wire::ReplySink::kStatusDeclined) { - SessionFail(MGFatalFamily::ReadbackDeclined, "MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no " - "GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance " - "row (R-5) but a blocking readback has no pixels to return, so it is a " - "Fatal here rather than a buffer of stale bytes"); - } - if (status != Wire::ReplySink::kStatusOk) { - SessionFail(MGFatalFamily::ReplyStatusInvalid, "MGPipe: Fatal{ReplyStatusInvalid, \"ReadPixels\"} - unknown reply status %d", status); - } - if (replySize != expected) { - SessionFail(MGFatalFamily::ReadbackReplyShort, "MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK " - "reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row " - "23's exact extent); the missing rows would otherwise be scattered as " - "whatever the destination held", - static_cast(replySize), - static_cast(expected)); - } + // Installed at static init, like the record seam's hooks (FatalFunnel.cpp): a library with + // MG_Remote linked resolves the emitters' session here before any verb can run. + [[maybe_unused]] const bool g_verbSessionResolverInstalled = [] { + MG_Record::SetVerbSessionResolver(&ResolveVerbSession); return true; - } - -#include "TextureReadbackEmit.inc" - - void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, - GLenum type, void* pixels) { - VerbChannel session = ChannelFor("ReadPixels"); - - // A bound PACK buffer makes pixels an offset. The owned reply is - // packed on this thread and only the requested rows are uploaded. - const auto pbo = MG_State::pGLContext != nullptr - ? MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject() - : SharedPtr{}; - - BeforeReadOnlyVerb(); - - if (width <= 0 || height <= 0) return; - const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type); - - PixelStoreParameters pack{}; - if (MG_State::pGLContext != nullptr) { - pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false); - } - ReadbackLayout layout; - const SizeT pboOffset = reinterpret_cast(pixels); - if (!CheckedReadbackLayout(width, height, 1, bytesPerPixel, pack, false, layout) || - (pbo && (pboOffset > pbo->GetSize() || layout.End > pbo->GetSize() - pboOffset))) { - RecordReadbackRangeError("ReadPixels"); - return; - } - - // P7 GATE 5 (g5-readback): A READ LARGER THAN ONE REPLY SLOT IS BANDED, NOT REFUSED. - // KHR-GL46.direct_state_access.renderbuffers_storage reads 256x512 RGBA/FLOAT = - // 2,097,152 bytes, sixteen over MaxReplyBytes, and that used to be - // Fatal{ReplyTooLarge} on every split arm while the monolith passed. Each band below - // is an ordinary read_pixels record - its own box, and a DstSize that IS its tight - // w*h*bpp extent (ID-49) - so every answer still fits one slot (ID-47) and the - // server's PH-3 bound (its tight answer against its own LinkTerms.maxReplyBytes, - // PipeApplier.cpp OnReadPixels) holds unchanged: no reply is chunked, the READ is. The cap is the link's own, read live, exactly as - // before, so a different SEG_REPLY geometry or a stream link needs no edit here. - ReadbackBandPlan plan; - if (!PlanReadbackBands(static_cast(width), static_cast(height), - bytesPerPixel, session.MaxReplyBytes(), plan)) { - // NOT EVEN ONE PIXEL FITS (or there is no reply pool at all). That is the only - // read no banding can answer, and ID-47's named refusal is kept for it - for the - // one-pixel piece that cannot be sent, so the message names what was impossible. - session.RequireReadPixelsReplyFits(1, 1, static_cast(format), - static_cast(type), bytesPerPixel); - return; - } - - // One band's source record, the same for both destinations below. - const auto bandInfo = [&](const ReadbackBand& band, Uint64 bandBytes) { - MG_Pipe::MGPReadbackInfo info{}; - info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers" - info.Box = MG_Pipe::MGPBox{ReadbackBandOrigin(x, band.FirstColumn), - ReadbackBandOrigin(y, band.FirstRow), 0, - static_cast(band.Columns), - static_cast(band.Rows), 1}; - info.Format = static_cast(format); - info.Type = static_cast(type); - info.Target = 0; - info.Level = 0; - info.DstOffset = 0; - info.DstSize = bandBytes; - return info; - }; - - // P9 (W1, CONTRACT-P9.md §1): A PACK BUFFER IS ANSWERED BY NOTHING. Each band crosses - // as read_pixels_to_buffer naming the buffer and the band's place in it - the - // layout above, per band - and the client neither waits nor copies: the server lands - // the rows, and the mark (taken first, like every producer row's) is what sends the - // next CPU read of the buffer through resource_readback. The bands are the reply - // form's for the SERVER's sake, not the wire's: the server reads each one into its - // scratch before landing it, and the reply cap is the bound it already enforces on - // that scratch (PH-3). The monolith arm is untouched - it maps the driver PBO back - // inside its own ReadPixels, as it always has. - if (pbo) { - const MG_Pipe::MGPipeHandle target = PackBufferReadbackTarget(pbo, pboOffset + layout.End); - if (!MG_Pipe::MGPipeHandleIsNull(target)) { - MarkReadPixelsPackBuffer(); - const Uint32 swapGroup = ReadbackSwapGroup(type, pack); - ForEachReadbackBand(static_cast(width), static_cast(height), plan, - [&](const ReadbackBand& band) { - MG_Pipe::MGPReadbackToBuffer record{}; - record.Src = bandInfo(band, TightReadbackBytes(static_cast(band.Columns), - static_cast(band.Rows), - format, type)); - record.Dst = target; - record.DstBase = pboOffset + layout.Start + band.FirstRow * layout.RowStride + - band.FirstColumn * bytesPerPixel; - record.RowStride = layout.RowStride; - record.ImageStride = 0; - record.SwapGroup = swapGroup; - session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixelsToBuffer, &record, - sizeof(record), nullptr, 0, nullptr, 0, nullptr); - }); - return; - } - } - - // THE COMMON CASE KEEPS THE ZERO-COPY. A neutral pack state means the destination - // layout IS the tight layout, so each band's reply lands straight in the - // application's buffer at the band's tight offset (whole-width bands and single-row - // pieces are both contiguous there). It is a fast path for the SAME bytes, not a - // second rule: ScatterReadbackBandIntoPackState is a memcpy of the band in exactly - // this case, and the control drives that function. - const Bool direct = !pbo && ReadbackPackStateIsTight(width, bytesPerPixel, pack); - // The bounce is the price of the application having asked for a layout. It is ONE - // band - at most a reply slot - rather than the whole read, and it is freed before - // this returns: R-11's rule one level out, nothing here outlives the call. - Vector bounce; - Bool pboSynced = false; - ForEachReadbackBand(static_cast(width), static_cast(height), plan, - [&](const ReadbackBand& band) { - // ONE tight-size function for production AND the control (M3 / codex 10a): the - // number the server allocates and the number the test asserts come from the - // same body, per band now. - const Uint64 bandBytes = TightReadbackBytes(static_cast(band.Columns), - static_cast(band.Rows), - format, type); - MG_Pipe::MGPReadbackInfo info = bandInfo(band, bandBytes); - - // CHECKED BEFORE THE EMISSION, not after the answer (ID-47), through S1's - // helper, for every record. The plan makes it true by construction; it stays - // because it is the one check that names the read at the call site if the plan - // and the pool ever disagree, where the server's PH-3 refusal would only answer - // ERROR on the apply thread. - session.RequireReadPixelsReplyFits(static_cast(band.Columns), - static_cast(band.Rows), - static_cast(format), - static_cast(type), bandBytes); - - Int32 status = 0; - Uint64 replySize = 0; - if (direct) { - Uint8* at = pixels == nullptr - ? nullptr - : static_cast(pixels) + - (band.FirstRow * static_cast(width) + band.FirstColumn) * - bytesPerPixel; - session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), - nullptr, 0, at, bandBytes, &status, &replySize); - // M2 / codex 11: an OK reply that arrived short, or a DECLINE/ERROR, must not - // be handed back as pixels. The Fatal aborts before the application reads the - // buffer, so the bytes EmitAndWait already copied are never observed. - if (!RequireReadbackReplyComplete(status, replySize, bandBytes)) return; - ApplyReadbackByteSwap(at, bandBytes, type, pack); - return; - } - - if (bounce.size() < bandBytes) bounce.resize(static_cast(bandBytes)); - session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, - 0, bounce.data(), bandBytes, &status, &replySize); - // BEFORE THE SCATTER, so a short or non-OK reply never reaches the application's - // pointer at all (the bounce is the only thing that held the partial bytes). - if (!RequireReadbackReplyComplete(status, replySize, bandBytes)) return; - ApplyReadbackByteSwap(bounce.data(), bandBytes, type, pack); - if (pbo) { - // pixels is an offset, never a host pointer. Upload only the requested - // rows so PACK padding and untouched bytes survive; the offsets are the - // overflow-checked layout's, the same numbers the range check accepted. - if (!pboSynced) { - pbo->SyncGpuWrites(); - pboSynced = true; - } - const Uint64 bandRowBytes = band.Columns * bytesPerPixel; - for (Uint64 row = 0; row < band.Rows; ++row) { - pbo->UploadSubData({bounce.data() + row * bandRowBytes, - static_cast(bandRowBytes)}, - pboOffset + layout.Start + - (band.FirstRow + row) * layout.RowStride + - band.FirstColumn * bytesPerPixel); - } - return; - } - ScatterReadbackBandIntoPackState(bounce.data(), pixels, width, band, bytesPerPixel, - pack); - }); - } + }(); void EmitPresent() { ClientSession& session = RequireSession("Present"); @@ -1414,368 +248,6 @@ namespace MobileGL::MG_Remote::Client { LogE2ControlLine(g_presentOrdinal); } - // ============================================================================= - // CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block - // (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows. - // ============================================================================= - // - // RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record - // carries the GL arguments verbatim - the enums as tokens, the GL names the backend - // keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's - // ServerVerbSink reproduces the backend call the monolith makes. The backend keeps - // reading the frontend state it reads today through the BARRIER-PULLED fields of its - // verb class, which the record's own verb stamp is what makes legal. So a migration is - // one emitter here plus one sink body there, and nothing in the backend moves. - // - // THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks - // below dispatch on the GL NAME - that is the whole point of carrying it - so a handle - // is carried for P7's sake only. FindByLifetimeId answers the handle the resource - // subsystem has already published for this object and kMGPipeNullHandle when it has - // published none; Acquire would MINT one here instead, at a call site that emits no - // create record, and the server would then be handed an identity it has never seen. - // A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a - // true statement, rather than a slot nobody allocated. - - MG_Pipe::MGPipeHandle PublishedTextureHandle( - const SharedPtr& texture) { - if (!texture) return MG_Pipe::kMGPipeNullHandle; - return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture, - texture->GetLifetimeId()); - } - - // glBindImageTexture. Emitted AT THE CALL, after the frontend has written the unit's - // ImageTextureBinding and MGP_FILL(BindImageTexture) has run - the record's verb - // boundary is what makes the server's read of that binding legal. set_shader_images - // (the draw-prep set) still travels at the next validate, untouched: that record - // describes a resolved unit for the draw, this one reproduces a call. - // - // NO PRE-VERB HOOK, AND THAT IS DELIBERATE. b1's two hooks describe "the work the - // record is ABOUT TO START" - PushPersistentMapsBeforeVerb publishes bytes an - // application wrote through a coherent map, MarkGpuWrites* builds the GPU-write set. - // A bind starts no shader and reads no buffer; the dispatch that later reads this image - // is the verb that carries both hooks, and running them here as well would push the - // same maps twice per dispatch and inflate b1's per-row counters. - void EmitBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, - GLint layer, GLenum access, GLenum format) { - VerbChannel session = ChannelFor("BindImageTexture"); - - MG_Pipe::MGPImageBind record{}; - // The unit's binding is the frontend's and has just been written by the caller, so - // the texture this record names is the one the server's SyncImageTextureBinding - // will pull for the same unit. Read from MG_State::pGLContext and NOT through - // gPipeInputs: on this side of a split gPipeInputs is gPipeInputs, which is the SERVER's - // view, and the client asking it a question is how the two halves come to disagree. - if (MG_State::pGLContext != nullptr) { - record.Res = PublishedTextureHandle( - MG_State::pGLContext->GetImageTextureBinding(static_cast(unit)).Texture); - } - record.Unit = static_cast(unit); - // The GL name the application passed, verbatim - what the ES slot is handed as - // `texture` and currently ignores. Never an identity (ARCHITECTURE 4.2.1). - record.GlName = static_cast(texture); - record.Level = static_cast(level); - record.Layer = static_cast(layer); - // THE GL ACCESS TOKEN, not MGPImageView::Access's three-value encoding (table 0's - // MGPImageBind::Access row). Two records, two jobs. - record.Access = static_cast(access); - record.Format = static_cast(format); - record.Layered = layered != GL_FALSE ? 1 : 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::BindShaderImage, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - // glDispatchCompute. THE HOOK ORDER IS b1's AND IS INHERITED FROM THE CLASS-C STUB - // VERBATIM: push the persistent maps (they produce resource_subdata records that must - // precede the verb on SEG_CMD), then the dispatch mark walk, then the record. The stub - // carried both calls before its Fatal precisely so that the package which flipped this - // slot would inherit a call site that was already correct. - void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { - VerbChannel session = ChannelFor("DispatchCompute"); - MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); - MarkGpuWritesForDispatch(); - - MG_Pipe::MGPGridInfo record{}; - record.GridX = static_cast(numGroupsX); - record.GridY = static_cast(numGroupsY); - record.GridZ = static_cast(numGroupsZ); - // Block* STAY 0 IN P5b (contract i1): the local size is a link artifact the backend - // reads from its own program, and a client-minted copy would be a second statement - // of it. P7's Magma may fill it from the reflection archive. - record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; - record.IndirectOffset = 0; - record.IsIndirect = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, - nullptr, 0, nullptr); - } - - void EmitDispatchComputeIndirect(GLintptr indirect) { - VerbChannel session = ChannelFor("DispatchComputeIndirect"); - MG_Record::PersistentMapTracker::Instance().PushDrawConsumers(); - MarkGpuWritesForDispatch(); - - MG_Pipe::MGPGridInfo record{}; - // The counts come from the GL_DISPATCH_INDIRECT_BUFFER, so the three grid fields are - // 0 and IsIndirect is what says so; the sink dispatches on it and calls - // glDispatchComputeIndirect with the offset the application spelled. - record.IsIndirect = 1; - record.IndirectOffset = static_cast(indirect); - record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; - if (MG_State::pGLContext != nullptr) { - const auto& bound = - MG_State::pGLContext - ->GetBufferBindingSlot(::MobileGL::BufferTarget::DispatchIndirect) - .GetBoundObject(); - if (bound) { - record.IndirectBuffer = MG_Pipe::MGPipeSlots().FindByLifetimeId( - MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId()); - } - } - session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, - nullptr, 0, nullptr); - } - - // glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has - // already validated them and already folds glTextureBarrier onto the same field - // (GL_Drawing.cpp:920-937), and Espryt's atomic-counter lowering - the counter bit - // implying the storage bit - stays inside the backend where the reason for it lives - // (DirectGLES.cpp:8837). A client that pre-lowered would be answering a driver question - // from the wrong side and the two arms would stop being byte-identical. - // - // No pre-verb hook: a barrier orders memory the GPU already holds. It starts no shader - // and reads no mapped buffer. - void EmitMemoryBarrier(GLbitfield barriers) { - VerbChannel session = ChannelFor("MemoryBarrier"); - MG_Pipe::MGPMemoryBarrier record{}; - record.Bits = static_cast(barriers); - record.ByRegion = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, - 0, nullptr, 0, nullptr); - } - - void EmitMemoryBarrierByRegion(GLbitfield barriers) { - VerbChannel session = ChannelFor("MemoryBarrierByRegion"); - MG_Pipe::MGPMemoryBarrier record{}; - record.Bits = static_cast(barriers); - record.ByRegion = 1; - session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, - 0, nullptr, 0, nullptr); - } - - // glCopyImageSubData -> resource_copy_region (53), which P5b rules is glCopyImageSubData - // ONLY (contract §6.4; the framebuffer-sourced copies are f1's row 76). - void EmitCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget, - GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget, - GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, - GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - VerbChannel session = ChannelFor("CopyImageSubData"); - - // The GL target selects the texture or renderbuffer handle namespace. - MG_Pipe::MGPCopyRegion record{}; - const auto handle = [](const MG_Backend::CopyImageEndpoint& endpoint) { - return endpoint.IsRenderbuffer() - ? MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Renderbuffer, - endpoint.Renderbuffer->GetLifetimeId()) - : PublishedTextureHandle(endpoint.Texture); - }; - record.Src = handle(src); - record.Dst = handle(dst); - // GL names remain diagnostic only; backend identity comes from handles. - record.SrcGlName = - src.IsRenderbuffer() ? src.Renderbuffer->GetExternalIndex() : src.Texture->GetExternalIndex(); - record.DstGlName = - dst.IsRenderbuffer() ? dst.Renderbuffer->GetExternalIndex() : dst.Texture->GetExternalIndex(); - // The GL targets verbatim, in a Uint16 - every GL texture target fits one. NOT - // MGPipeResourceTarget: the sink only ever forwards these to a slot that takes GL - // enums, and the tree has no resource-target -> GL-enum inverse to spend on them. - record.SrcTarget = static_cast(srcTarget); - record.DstTarget = static_cast(dstTarget); - record.SrcLevel = static_cast(srcLevel); - record.DstLevel = static_cast(dstLevel); - // SrcBox is {srcX, srcY, srcZ, w, h, d}: the source origin AND the extent, which is - // one extent for both endpoints (GL spells the copy's size once). - record.SrcBox = MG_Pipe::MGPBox{srcX, srcY, srcZ, static_cast(srcWidth), - static_cast(srcHeight), - static_cast(srcDepth)}; - record.DstX = dstX; - record.DstY = dstY; - record.DstZ = dstZ; - session.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCopyRegion, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - // glShaderStorageBlockBinding -> set_storage_block_binding (75), the ONE content-carrying - // row P5b adds. The block is NAMED, not indexed, because the application's index is the - // frontend interface-query enumeration's and no backend shares that index space - // (BackendObject.h:216-221) - the name is the one coordinate all three agree on. - void EmitShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, - GLuint storageBlockBinding) { - VerbChannel session = ChannelFor("ShaderStorageBlockBinding"); - // The backend slot's own first line (DirectGLES.cpp:9201), kept here so a null name - // never becomes a zero-size blob - which rule A forbids spelling at all. - if (storageBlockName == nullptr) return; - BeforeReadOnlyVerb(); - - MG_Pipe::MGPStorageBlockBinding record{}; - record.GlName = static_cast(program); - record.Binding = static_cast(storageBlockBinding); - record.ShaderCso = MG_Pipe::kMGPipeNullHandle; - if (MG_State::pGLContext != nullptr) { - const auto& programObject = MG_State::pGLContext->GetProgramObject(program); - if (programObject) { - // The application may rebind a stage program before it is - // current or attached to a pipeline. A minted slot alone does - // not publish its archive; this verb must follow that birth. - programObject->JoinLinkAndSpirv(); - Uint64 bytes = 0; - auto& emitter = MG_Pipe::MGPipeProgramEmitterInstance(); - record.ShaderCso = emitter.AcquireShaderCso(*programObject, bytes); - emitter.EmitProgramBindings(*programObject, record.ShaderCso); - } - } - // Size = strlen + 1: THE NUL TRAVELS (contract table 0's block-name row). The - // decoder re-terminates into a bounded local and refuses a run whose last byte is - // not NUL, so the two sides agree on where the name ends. - const Uint64 nameBytes = static_cast(std::strlen(storageBlockName)) + 1ull; - record.Name = session.StageBytes(storageBlockName, nameBytes); - session.EmitAndWait(MG_Pipe::MGPWireOp::SetStorageBlockBinding, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - // CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the - // tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2). - // ============================================================================= - // - // SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a - // control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets - // the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(), - // which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no - // shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE - // TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span, - // after the record and before GLContext::EndTransformFeedback clears the live bindings, - // which is exactly where b1 already put it (GL_Drawing.cpp's - // MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same - // buffers twice and taking it INSTEAD of there would mark nothing. - // - // P5f fe extends Begin with the immutable capture snapshot: program CSO handle, - // XFB object lifetime id, and four buffer handle/range pairs. Buffer resource records - // precede this verb through BeforeReadOnlyVerb; AcquireShaderCso below publishes the - // program archive before Begin. Deferred driver Begin and End therefore need no - // frontend program or binding-point lookup. set_stream_output_targets stays unused: - // this state changes at the capture-span boundary, so it rides Begin itself. - - void EmitBeginTransformFeedback(GLenum primitiveMode) { - VerbChannel session = ChannelFor("BeginTransformFeedback"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPStreamOutputBegin record{}; - // The GL token verbatim (contract table 0's "GL enums on the wire"): the sink hands - // it to the backend slot that takes it, and nothing between here and there reads it. - record.PrimitiveMode = static_cast(primitiveMode); - auto& context = *MG_State::pGLContext; - record.LifetimeId = context.GetBoundTransformFeedbackLifetimeId(); - const auto& program = context.GetTransformFeedbackProgram(); - if (program) { - Uint64 bytes = 0; - record.CaptureProgram = MG_Pipe::MGPipeProgramEmitterInstance().AcquireShaderCso(*program, bytes); - } - static_assert(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS == 4); - for (Uint i = 0; i < 4; ++i) { - const auto& point = context.GetBufferBindingPoint(BufferTarget::TransformFeedback, i); - const auto& buffer = point.GetBoundObject(); - if (!buffer) continue; - const auto range = point.GetRange(); - const SizeT start = std::min(range.start, buffer->GetSize()); - const SizeT end = std::min(range.end, buffer->GetSize()); - record.Targets[i] = {MG_Pipe::MGPipeResourceTrackerInstance().Find(*buffer), start, - end > start ? end - start : 0}; - } - session.EmitAndWait(MG_Pipe::MGPWireOp::BeginStreamOutput, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitEndTransformFeedback() { - VerbChannel session = ChannelFor("EndTransformFeedback"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPXfbAccounting record{}; - // THE ACCOUNTING IS THE CLIENT'S OWN AND IT IS INFORMATIONAL ON THIS SIDE OF THE - // WIRE: end_stream_output's backend call takes no arguments, and the three numbers - // are what the frontend has counted over this span (CONTRACT-P5B.md §2 t2, the - // companions row). They travel because the row has carried them since P4a and - // because they are what a server-side scatter would need when P9 lands one; the - // sink today calls GL.EndTransformFeedback() and reads none of them. Read here, - // BEFORE GLContext::EndTransformFeedback resets the counters at the call site. - const auto& context = *MG_State::pGLContext; - record.CapturedVertices = context.GetTransformFeedbackCapturedVertices(); - record.PrimitivesWritten = context.GetTransformFeedbackPrimitiveCounter(); - record.PrimitiveMode = static_cast(context.GetTransformFeedbackPrimitiveMode()); - session.EmitAndWait(MG_Pipe::MGPWireOp::EndStreamOutput, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitPauseTransformFeedback() { - VerbChannel session = ChannelFor("PauseTransformFeedback"); - BeforeReadOnlyVerb(); - - // Reserved IS zero and the contract says so (MGPStreamOutputControl{Reserved = 0}). - // The row exists to BE the verb boundary - the stamp the server puts up before the - // sink runs - not to carry anything. - MG_Pipe::MGPStreamOutputControl record{}; - record.Reserved = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::PauseStreamOutput, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitResumeTransformFeedback() { - VerbChannel session = ChannelFor("ResumeTransformFeedback"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPStreamOutputControl record{}; - record.Reserved = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::ResumeStreamOutput, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitBindTransformFeedback(GLuint name) { - VerbChannel session = ChannelFor("BindTransformFeedback"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPStreamOutputBind record{}; - // THE GL NAME IS NOT AN IDENTITY (ARCHITECTURE 4.2.1) and is carried anyway, because - // it is the key the backend has always used: Espryt indexes its driver objects by it - // (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and generates the ES object on - // first bind. Name 0 is the default object, which is why the field is not a handle. - record.GlName = static_cast(name); - // Beside it, the identity that WILL dispatch: the frontend's per-object lifetime id, - // process-wide and never reused, which is what survives glGenTransformFeedbacks - // recycling a name. Read AFTER GLContext::BindTransformFeedbackObject at the call - // site, so it is the id of the object being bound and not of the previous one. - record.LifetimeId = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId(); - session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - - void EmitPatchParameteri(GLenum pname, GLint value) { - VerbChannel session = ChannelFor("PatchParameteri"); - BeforeReadOnlyVerb(); - - MG_Pipe::MGPPatchParameter record{}; - // GL_PATCH_VERTICES is the only pname that reaches a backend slot - the frontend - // answers GL_PATCH_DEFAULT_*_LEVEL itself and bakes those into the synthesized - // control stage - and the frontend has already rejected every other pname with - // INVALID_ENUM before this call (GL_Drawing.cpp's PatchParameteri). Carried verbatim - // so the sink reproduces the call rather than a reading of it. - record.Pname = static_cast(pname); - record.Value = static_cast(value); - // set_patch_state (43) STILL TRAVELS, at the next validate, and that is not a - // duplicate: it is the applier's working-block copy, this is the driver push Espryt - // does AT THE CALL (DirectGLES.cpp:8760), and both pushes happen today on the - // monolith path too (CONTRACT-P5B.md §2 t2). - session.EmitAndWait(MG_Pipe::MGPWireOp::PatchParameter, &record, sizeof(record), - nullptr, 0, nullptr, 0, nullptr); - } - // ============================================================================= // CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER. // ============================================================================= @@ -2168,7 +640,7 @@ namespace MobileGL::MG_Remote::Client { "the three classes no longer partition the 71 slots"); MG_Backend::GlobalBackendFunctionsTable BuildRemoteEmitTable() { - ArmControlKnobs(); + MG_Record::ArmVerbControlKnobs(); MG_Backend::GlobalBackendFunctionsTable table{}; // ---- class C first, so that a slot forgotten below stays Fatal rather than null. @@ -2197,7 +669,6 @@ namespace MobileGL::MG_Remote::Client { table.GL.GetQueryResult64 = &EmitGetQueryResult64; table.GL.DeleteBackendQuery = &EmitDeleteBackendQuery; table.GL.GetGpuTimestampNs = &EmitGetGpuTimestampNs; - table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback; // ---- class A table.GL.GetIntegeri_v = &AnswerGetIntegeri_v; @@ -2209,33 +680,11 @@ namespace MobileGL::MG_Remote::Client { table.GL.PrefersCpuXfbPrimitiveAccounting = CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting(); - // ---- class B - table.GL.Clear = &EmitClear; - table.GL.DrawArrays = &EmitDrawArrays; + // ---- class B. P13 W5: every slot a VerbChannel emitter serves (draws, clears, + // blits, readbacks, compute, copies, image binds, XFB, mipmaps) is MG_Record's + // (MG_Impl/Pipe/Verb/VerbPort.cpp); the session's own slots stay below. + MG_Record::AssignVerbEmitters(table); // ---- P5b d1: the nineteen draw slots, all on draw_vbo (CONTRACT-P5B.md §2 d1) - table.GL.DrawElements = &EmitDrawElements; - table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; - table.GL.DrawRangeElements = &EmitDrawRangeElements; - table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; - table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; - table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; - table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; - table.GL.DrawElementsInstancedBaseVertexBaseInstance = - &EmitDrawElementsInstancedBaseVertexBaseInstance; - table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; - table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; - table.GL.MultiDrawArrays = &EmitMultiDrawArrays; - table.GL.MultiDrawElements = &EmitMultiDrawElements; - table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; - table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; - table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; - table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; - table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; - table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; - table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; - table.GL.ReadPixels = &EmitReadPixels; - table.GL.BlitFramebuffer = &EmitBlitFramebuffer; - table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer; table.Present = &EmitPresent; // P10 B: the swap interval is the server's presentation-path question, so the slot // forwards it there as a SurfaceControlFrame (ServerLoop.cpp's SetSwapInterval arm), @@ -2246,36 +695,10 @@ namespace MobileGL::MG_Remote::Client { // the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot // whose row is removed from MGR_UNMIGRATED_T2_SLOTS but not assigned here would be // NULL and caught by RemoteEmitTable.NoSlotIsNull rather than silently skipped. - table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback; - table.GL.EndTransformFeedback = &EmitEndTransformFeedback; - table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback; - table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback; - table.GL.BindTransformFeedback = &EmitBindTransformFeedback; - table.GL.PatchParameteri = &EmitPatchParameteri; // ---- f1 ---- - table.GL.ClearBufferfi = &EmitClearBufferfi; - table.GL.ClearBufferfv = &EmitClearBufferfv; - table.GL.ClearBufferiv = &EmitClearBufferiv; - table.GL.ClearBufferuiv = &EmitClearBufferuiv; - table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; - table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; - table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; - table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; - table.GL.CopyTexImage2D = &EmitCopyTexImage2D; - table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; - table.GL.GenerateMipmap = &EmitGenerateMipmap; - table.GL.GetTexImage = &EmitGetTexImage; - table.GL.GetTextureImage = &EmitGetTextureImage; // ---- class B, P5b package i1 (kEmittedSlotsI1 = 7) - table.GL.BindImageTexture = &EmitBindImageTexture; - table.GL.DispatchCompute = &EmitDispatchCompute; - table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect; - table.GL.MemoryBarrier = &EmitMemoryBarrier; - table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion; - table.GL.CopyImageSubData = &EmitCopyImageSubData; - table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; return table; } @@ -2322,268 +745,8 @@ namespace MobileGL::MG_Remote::Client { return table; } - void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table) { - // P13 W4a: the draw and dispatch families. Later W4 steps add their verbs here AND to - // ApplyOnMonolithPort's switch, in the same commit. - table.GL.DrawArrays = &EmitDrawArrays; - table.GL.DrawElements = &EmitDrawElements; - table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; - table.GL.DrawRangeElements = &EmitDrawRangeElements; - table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; - table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; - table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; - table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; - table.GL.DrawElementsInstancedBaseVertexBaseInstance = &EmitDrawElementsInstancedBaseVertexBaseInstance; - table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; - table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; - table.GL.MultiDrawArrays = &EmitMultiDrawArrays; - table.GL.MultiDrawElements = &EmitMultiDrawElements; - table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; - table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; - table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; - table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; - table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; - table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; - table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; - table.GL.DispatchCompute = &EmitDispatchCompute; - table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect; - // P13 W4b: the texture family - its record arm resolves the texture from the verb's own - // handle (VerbMipRes, VerbCopyTexDst, the copy's two endpoints). - table.GL.GenerateMipmap = &EmitGenerateMipmap; - table.GL.CopyTexImage2D = &EmitCopyTexImage2D; - table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; - table.GL.CopyImageSubData = &EmitCopyImageSubData; - table.GL.ReadPixels = &EmitReadPixels; - table.GL.GetTexImage = &EmitGetTexImage; - table.GL.GetTextureImage = &EmitGetTextureImage; - // P13 W4c: framebuffer verbs name their framebuffers by handle (MGPClear::Fbo, the blit's - // pair), image binds and storage-block bindings carry their objects' handles. - table.GL.Clear = &EmitClear; - table.GL.ClearBufferfi = &EmitClearBufferfi; - table.GL.ClearBufferfv = &EmitClearBufferfv; - table.GL.ClearBufferiv = &EmitClearBufferiv; - table.GL.ClearBufferuiv = &EmitClearBufferuiv; - table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; - table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; - table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; - table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; - table.GL.BlitFramebuffer = &EmitBlitFramebuffer; - table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer; - table.GL.BindImageTexture = &EmitBindImageTexture; - table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; - // The XFB family: the span's capture program and targets ride Begin; the object is named - // by lifetime id. It follows the program family because Begin names the capture program - // by its shader CSO, whose archive monolith records carry from W4c. - table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback; - table.GL.EndTransformFeedback = &EmitEndTransformFeedback; - table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback; - table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback; - table.GL.BindTransformFeedback = &EmitBindTransformFeedback; - table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback; - g_monolithVerbPort = true; - } - - Bool MonolithVerbPortInstalled() { return g_monolithVerbPort; } - Uint32 ImplementedVerbCount() { return kEmittedSlots; } Uint32 LocallyAnsweredSlotCount() { return kLocallyAnsweredSlots; } Uint32 UnmigratedSlotCount() { return kUnmigratedSlots; } - void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; } - Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; } - - void SetDropDrawEmissionForNegativeControl(Bool drop) { g_dropDrawEmission = drop; } - Uint64 DroppedDrawEmissions() { return g_droppedDrawEmissions; } - - Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel, - const PixelStoreParameters& pack) { - return ReadbackPackStateIsTight(width, bytesPerPixel, pack); - } - - Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type) { - return TightReadbackBytes(width, height, format, type); - } - - // M2 / codex 11's predicate at namespace scope: the one RequireReadbackReplyComplete decides - // on and the one the control drives. 0=OK / 1=DECLINED / 2=ERROR. - Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected) { - return status == Wire::ReplySink::kStatusOk && replySize == expected; - } - - // g5-readback's plan (EmitTables.h, ReadbackBand). Pixels-per-reply first and the row test - // against it, so no product here can overflow whatever the inputs: a unit control drives - // this with Uint64 extremes, and the emitter with a GLsizei width. - Bool PlanReadbackBands(Uint64 width, Uint64 height, Uint64 bytesPerPixel, Uint64 maxReplyBytes, - ReadbackBandPlan& plan) { - if (width == 0 || height == 0 || bytesPerPixel == 0 || maxReplyBytes < bytesPerPixel) { - return false; - } - const Uint64 pixelsPerReply = maxReplyBytes / bytesPerPixel; // >= 1 - if (width <= pixelsPerReply) { - // floor(floor(cap / bpp) / width) == floor(cap / (bpp * width)): the most whole rows - // one reply holds, and at least one because a row fits. - plan.RowsPerBand = pixelsPerReply / width; - plan.ColumnsPerBand = width; - } else { - // A SINGLE ROW IS LARGER THAN A REPLY (a >131071-pixel RGBA/FLOAT row at the 2 MiB - // slot - wider than any attachment, but a legal glReadPixels whose in-bounds pixels - // GL still defines). Each row is cut into pieces; one row per band keeps every piece - // contiguous in the tight layout. - plan.RowsPerBand = 1; - plan.ColumnsPerBand = pixelsPerReply; - } - return true; - } - - // ID-49's scatter, for one band of a banded read (g5-readback). The whole-read scatter below - // is this function over the single band {0, height, 0, width}, so there is one copy of - // 8.4.4's arithmetic and the control drives it. - void ScatterReadbackBandIntoPackState(const void* band, void* destination, GLsizei width, - const ReadbackBand& where, Uint64 bytesPerPixel, - const PixelStoreParameters& pack) { - if (band == nullptr || destination == nullptr || width <= 0 || where.Rows == 0 || - where.Columns == 0) { - return; - } - const Uint64 rowPixels = - pack.RowLength > 0 ? static_cast(pack.RowLength) : static_cast(width); - const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; - const Uint64 strideBytes = - ((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment; - // m6: the client re-derives GL 4.6 8.4.4's pack layout, so it honours GL_PACK_ROW_LENGTH - // VERBATIM - including the ill-formed 0 < ROW_LENGTH < width, where the row stride is - // narrower than a written row and consecutive rows overlap. GL leaves that case to the - // implementation; the client reproduces exactly what the monolith backend's own scatter - // would do with the same state rather than clamping, so the two arms stay byte-identical. - // The fast path (ReadbackPackStateIsTight) already rejects any ROW_LENGTH != width, so - // this only runs on the scatter path the application asked for. - const Uint64 writtenPerRow = where.Columns * bytesPerPixel; - // SKIP_IMAGES and IMAGE_HEIGHT ARE IGNORED (codex 6). glReadPixels is a 2-D read; GL - // does not apply the image-level pack parameters to it, and the monolith conversion path - // says so explicitly with honorPackImageParams=false (DirectGLES.cpp:10905). Applying - // SKIP_IMAGES here shifted a read with SKIP_IMAGES=1 by a whole image and overran an - // application buffer sized for exactly `height` rows. Only SKIP_ROWS and SKIP_PIXELS - - // the 2-D skips - offset the first written byte; the band's own row and column then - // offset it within the read, on the SAME stride, so a banded read writes exactly the - // bytes the whole-read scatter would have, in the same row order. - auto* out = static_cast(destination) + - (static_cast(pack.SkipRows) + where.FirstRow) * strideBytes + - (static_cast(pack.SkipPixels) + where.FirstColumn) * bytesPerPixel; - const auto* in = static_cast(band); - for (Uint64 row = 0; row < where.Rows; ++row) { - std::memcpy(out + row * strideBytes, in + row * writtenPerRow, - static_cast(writtenPerRow)); - } - } - - // ID-49's scatter. Exported for the same reason as the refusal above: the control drives - // THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic. - void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width, - GLsizei height, Uint64 bytesPerPixel, - const PixelStoreParameters& pack) { - if (tight == nullptr || destination == nullptr || width <= 0 || height <= 0) return; - ScatterReadbackBandIntoPackState( - tight, destination, width, - ReadbackBand{0, static_cast(height), 0, static_cast(width)}, - bytesPerPixel, pack); - } - - // ============================================================================= - // P5b d1 - the draw family's record plan, at namespace scope so the unit cases drive - // exactly what the emitters above call (R-16). - // ============================================================================= - - Uint8 RemoteIndexSizeFor(GLenum indexType) { - switch (indexType) { - case GL_UNSIGNED_BYTE: return 1; - case GL_UNSIGNED_SHORT: return 2; - case GL_UNSIGNED_INT: return 4; - default: return 0; - } - } - - MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, - GLuint baseInstance, Uint32 numDraws, - const RemoteDrawBindings& bindings) { - MG_Pipe::MGPDrawInfo info{}; - info.Mode = static_cast(mode); - info.IndexSize = indexSize; - // The restart state rides verbatim (informational in P5b: the backend's - // ScopedRestartIndexSubstitution reads its own barrier-pulled copy and the index bytes - // on its side). kDrawHasUserIndices / kDrawIsIndirect are added by the emission itself, - // kDrawHasIndexRange by the two DrawRangeElements* callers. - info.Flags = bindings.PrimitiveRestart ? static_cast(MG_Pipe::kDrawPrimitiveRestart) : 0; - // P5e (vi), CONTRACT-P5E §2.1 (ii) / §5.1. On the wire BECAUSE the server needs it: the - // barriered predicate is computed identically by both roles from the record alone, and - // "does this draw fetch from client memory" is not derivable from anything else in it - - // a null Res inside the vertex-buffer window is also what a DISABLED attribute below - // the high-water mark publishes. Set for every draw shape, not just the array ones: a - // glDrawElements can fetch its VERTICES from client memory too. - if (bindings.ClientVertexArrays) info.Flags |= static_cast(MG_Pipe::kDrawClientArrays); - // A negative count has already been refused by the frontend (INVALID_VALUE); 0 crosses - // as 0 and draws nothing, which is what the driver does with it. - info.InstanceCount = instanceCount > 0 ? static_cast(instanceCount) : 0u; - info.StartInstance = baseInstance; - info.RestartIndex = bindings.RestartIndex; - info.DrawIdOffset = 0; - // The handle beside the call (rule D): the VAO's element buffer for an indexed draw, - // the same handle set_index_buffer (32) carried at validate; null for arrays and for a - // client index array. - info.IndexResource = (indexSize != 0 && bindings.ElementBufferBound) ? bindings.ElementBuffer - : MG_Pipe::kMGPipeNullHandle; - info.MinIndex = ~0u; // "unknown" (MGPipeTypes.h); the DrawRangeElements* callers fill it - info.MaxIndex = ~0u; - info.XfbCpuCapturedVertices = 0; - info.NumDraws = numDraws; - return info; - } - - Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, - GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out) { - out = MG_Pipe::MGPDrawRange{}; - out.Count = count > 0 ? static_cast(count) : 0u; - if (indexSize == 0) { - // Arrays: `first`, spelled through the pointer parameter so one function serves - // both shapes. A negative first has already been refused by the frontend. - const auto first = static_cast(reinterpret_cast(indicesOrFirst)); - out.Start = first > 0 ? static_cast(first) : 0u; - out.IndexBias = 0; - return true; - } - out.IndexBias = baseVertex; - if (!bindings.ElementBufferBound) { - // A client index array: the emitter stages the bytes and the run starts at its - // first index. - out.Start = 0; - return true; - } - // An element buffer: `indices` is a byte offset into it, and Start is that offset in - // INDICES - the same arithmetic OnDrawVbo inverts (offset = Start * IndexSize). An - // offset that is not a whole number of indices cannot be spelled and is the caller's - // refusal, not a rounding. - const auto offset = reinterpret_cast(indicesOrFirst); - if (offset % indexSize != 0) return false; - const std::uintptr_t start = offset / indexSize; - if (start > 0xFFFFFFFFull) return false; - out.Start = static_cast(start); - return true; - } - - MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, - GLsizei drawCount, GLsizei stride, - GLintptr parameterOffset, Bool hasParameterBuffer) { - MG_Pipe::MGPDrawIndirect block{}; - block.Buffer = bindings.DrawIndirectBuffer; - block.ParameterBuffer = hasParameterBuffer ? bindings.ParameterBuffer : MG_Pipe::kMGPipeNullHandle; - // The command byte offset the call passed as `indirect` (a bound GL_DRAW_INDIRECT_BUFFER - // makes it an offset, never an address - the emitter refused the other case by name). - block.Offset = static_cast(reinterpret_cast(indirect)); - block.ParameterOffset = hasParameterBuffer ? static_cast(parameterOffset) : 0u; - // 0 = tightly packed, as GL spells it; the backend normalises. Negative counts and - // strides have already been refused by the frontend. - block.Stride = stride > 0 ? static_cast(stride) : 0u; - block.DrawCount = drawCount > 0 ? static_cast(drawCount) : 0u; - return block; - } - } // namespace MobileGL::MG_Remote::Client diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h index e7c5dcf9..34729dcc 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.h +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -66,6 +66,7 @@ // the dependency the aliases have explicit. #include #include +#include namespace MobileGL::MG_Remote::Client { @@ -84,16 +85,31 @@ namespace MobileGL::MG_Remote::Client { // A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8). const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable(); - // Called by the Fatal slots. Named separately so a death test can filter on it and so - // that the message wording lives in exactly one place. - [[noreturn]] void UnmigratedVerbFatal(const char* slot); - - // P13 W4: monolith's record arm. Overwrites the ported verb slots of the backend's own - // table with this file's emitters, which hand their planned record to an in-process - // ServerVerbSink instead of a session (EmitTables.cpp, "THE MONOLITH VERB PORT"). Installed - // by MG_Backend's bring-up when MG_Config::RecordArmAliasesFrontend() holds. - void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table); - Bool MonolithVerbPortInstalled(); + // P13 W5: the verb emitters, the monolith verb port, the E2 controls and the readback / draw + // planners are MG_Record's (MG_Impl/Pipe/Verb/VerbPort.h) - the record arm runs them in a + // library without a transport. Re-exported here under the names the remote table's users + // and its unit cases already spell. + using MG_Record::UnmigratedVerbFatal; + using MG_Record::InstallMonolithVerbPort; + using MG_Record::MonolithVerbPortInstalled; + using MG_Record::SetDropClearEmissionForNegativeControl; + using MG_Record::DroppedClearEmissions; + using MG_Record::SetDropDrawEmissionForNegativeControl; + using MG_Record::DroppedDrawEmissions; + using MG_Record::TightReadbackByteCount; + using MG_Record::ScatterTightReadbackIntoPackState; + using MG_Record::ReadbackBand; + using MG_Record::ReadbackBandPlan; + using MG_Record::PlanReadbackBands; + using MG_Record::ForEachReadbackBand; + using MG_Record::ScatterReadbackBandIntoPackState; + using MG_Record::ReadbackPackStateIsTightForTest; + using MG_Record::ReadbackReplyIsComplete; + using MG_Record::RemoteDrawBindings; + using MG_Record::RemoteIndexSizeFor; + using MG_Record::PlanDrawInfo; + using MG_Record::PlanDrawRange; + using MG_Record::PlanDrawIndirect; // How many of the 71 slots have a real emitter. Reported at bring-up and asserted by the // gate: a table that silently loses an emitter should not be able to look the same as one @@ -111,189 +127,6 @@ namespace MobileGL::MG_Remote::Client { Uint32 LocallyAnsweredSlotCount(); // class A - answered from the caps mirror, R-15 Uint32 UnmigratedSlotCount(); // class C - Fatal{UnmigratedVerb} - // THE E2 NEGATIVE CONTROLS (t1's debt against c1, BRIEF §7). When set, the named emitter - // SKIPS its record - it still runs the pre-verb hooks and still returns - so a replay that - // is really going through the wire loses that verb while a replay that fell through to the - // driver is unaffected. They are functions rather than knobs in Config.h for two reasons: - // a control has to be settable from a test process that has already started, and a knob - // would be a MOBILEGL_IPC_-shaped name for something no operator may ever set. - // - // Emissions actually skipped, so a control can assert that it DID something rather than - // that a picture changed - a control that silently never fired is the third shape of R-16's - // "a gate that cannot go red for its own reason". - // - // WHICH ONE E2'S RETRACE USES, and it is not the clear. Measured on the joint head: with - // MOBILEGL_IPC_E2_DROP_CLEAR=1 armed and its WARN in the library's own log, the OpenRA - // retrace under inproc still scored ssim=1.000000 / mismatchPixels=0 (joint-v1.md §3), - // because OpenRA covers every pixel it clears before the snapshot. Dropping the DRAWS is - // the control whose observable the golden is actually made of. See EmitTables.cpp's - // ArmControlKnobs for the trace census that settled it. - void SetDropClearEmissionForNegativeControl(Bool drop); - Uint64 DroppedClearEmissions(); - void SetDropDrawEmissionForNegativeControl(Bool drop); - Uint64 DroppedDrawEmissions(); - - // ID-47. The CLIENT refuses a readback whose answer would not fit a reply slot, BEFORE it - // emits the record, and names the read. THE REFUSAL ITSELF IS s1's - - // ClientSession::RequireReadPixelsReplyFits, forwarding to - // ReplySlotPool::RequireReadPixelsFits - and this package does not own a second copy of the - // message: two spellings of one refusal is how the two sides come to disagree about which - // reads are legal. What c1 owns is the CALL SITE and the number it passes, which is ID-49's - // tight extent; the control below is over that, and s1-v3.md §1's cases are over the - // helper. - - // ID-49. `MGPReadbackInfo::DstSize` is the TIGHT w*h*bytesPerPixel extent - the reply - // payload - and nothing about the application's pack state crosses the wire. The server - // reads with a NEUTRAL pack state into that run; this is the number both sides derive. - Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type); - - // ID-49. Scatters the tight rows into the application's pointer per the application's own - // pack state (ROW_LENGTH, SKIP_*, ALIGNMENT), which only the client holds. Exposed for the - // same reason as the refusal above: the control drives the function the emitter calls - // rather than a second copy of GL 4.6 8.4.4's arithmetic. THE GAPS ARE NEVER WRITTEN - - // they belong to the application - and that is what the control checks with a sentinel. - void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width, - GLsizei height, Uint64 bytesPerPixel, - const PixelStoreParameters& pack); - - // P7 gate 5 (g5-readback). A glReadPixels whose tight answer is larger than one reply slot - // is NOT one record: the client splits the w*h rectangle into BANDS, emits each band as an - // ordinary read_pixels record with its own box and its own tight DstSize (so every answer - // still fits one slot, ID-47, and the server's PH-3 bound - its tight answer against its own - // LinkTerms.maxReplyBytes - holds unchanged), and scatters each band into the application's destination under its - // own GL_PACK_* state. No reply is chunked; the READ is. - // - // THE SHAPE. Whole-width row bands of floor(maxReplyBytes / rowBytes) rows when a row fits - // a reply; otherwise every row is cut into column pieces of floor(maxReplyBytes / bpp) - // pixels. Both shapes are contiguous in the tight layout, which is what lets the neutral- - // pack fast path keep reading straight into the application's pointer. Bands are in - // row-major order and cover the rectangle exactly once. - // - // NO PLAN exactly when not even ONE pixel fits a reply (maxReplyBytes < bytesPerPixel, - // including a link with no reply pool at all); the emitter then refuses BY NAME through - // ID-47's unchanged helper rather than emitting anything. That is the only ReadPixels shape - // left that ends in Fatal{ReplyTooLarge}: CONTRACT-P5 row 23 makes an answer that cannot fit - // a slot a named Fatal, not a GL error, and no pixel can be returned in pieces smaller than - // itself. - // - // THE PLAN IS TWO NUMBERS AND THE BANDS ARE WALKED, NOT LISTED: a band list would be one - // allocation per read that grows with the read, and the walk below is the only place the - // order and the coverage are decided - the emitter and the unit control both call it. - struct ReadbackBand { - Uint64 FirstRow = 0; - Uint64 Rows = 0; - Uint64 FirstColumn = 0; - Uint64 Columns = 0; - }; - struct ReadbackBandPlan { - Uint64 RowsPerBand = 0; // 1 when a single row does not fit a reply - Uint64 ColumnsPerBand = 0; // the whole width when it does - }; - // False (and `plan` untouched) exactly when not even one pixel fits a reply. - Bool PlanReadbackBands(Uint64 width, Uint64 height, Uint64 bytesPerPixel, Uint64 maxReplyBytes, - ReadbackBandPlan& plan); - template - void ForEachReadbackBand(Uint64 width, Uint64 height, const ReadbackBandPlan& plan, - Visit&& visit) { - if (plan.RowsPerBand == 0 || plan.ColumnsPerBand == 0) return; - for (Uint64 row = 0; row < height; row += plan.RowsPerBand) { - const Uint64 rows = plan.RowsPerBand < height - row ? plan.RowsPerBand : height - row; - for (Uint64 column = 0; column < width; column += plan.ColumnsPerBand) { - const Uint64 columns = - plan.ColumnsPerBand < width - column ? plan.ColumnsPerBand : width - column; - visit(ReadbackBand{row, rows, column, columns}); - } - } - } - - // The scatter for ONE band: `band` holds band.Rows x band.Columns tight pixels, and they land - // where ScatterTightReadbackIntoPackState would have put those pixels of the whole - // `width`-wide read. ScatterTightReadbackIntoPackState is this function over the single band - // {0, height, 0, width}, so the whole-read arithmetic and the banded one cannot drift. - void ScatterReadbackBandIntoPackState(const void* band, void* destination, GLsizei width, - const ReadbackBand& where, Uint64 bytesPerPixel, - const PixelStoreParameters& pack); - - // ID-49. True when the destination layout IS the tight layout, which is the only condition - // under which EmitReadPixels may read the reply straight into the application pointer and - // skip the bounce. Exported because the FAST PATH and the SCATTER have to agree, and the - // only honest way to state that is to drive both and compare - a case that tested either - // alone would pass a predicate that said yes to a layout the scatter would have rearranged. - Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel, - const PixelStoreParameters& pack); - - // M2 / codex 11. True exactly when the readback reply is OK and carries the read's own exact - // extent (CONTRACT-P5 row 23). EmitReadPixels calls this and Fatals by name when it is false - // - a short OK reply, or a DECLINE/ERROR with a zero payload, is refused rather than scattered - // as pixels. Exposed so the control drives the production predicate (R-16), not a copy: pass - // 0=OK / 1=DECLINED / 2=ERROR as `status`. - Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected); - - // ============================================================================= - // P5b d1 - the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1) - // ============================================================================= - // - // The nineteen indexed / instanced / multi-draw / indirect entry points all ride draw_vbo - // (59), and the ONLY thing that differs per entry point is how the GL arguments become the - // record's fields. That derivation is split out of the emitters as three pure functions - // over a snapshot of the bindings the emitter read from the GL context, so a unit case can - // drive the PRODUCTION derivation with synthetic bindings (R-16) while the integration lane - // proves the bindings are read from the right slots. The emitters compose these and add - // nothing but the hooks, the E2 draw-drop control and the staging of a client index array. - - // What the emitter reads from the GL context before it plans a draw. - struct RemoteDrawBindings { - // The VAO's GL_ELEMENT_ARRAY_BUFFER: bound or not, and its handle when bound (the same - // handle set_index_buffer carried at validate). Not bound means `indices` is a client - // pointer and the emitter stages the bytes (kDrawHasUserIndices). - Bool ElementBufferBound = false; - MG_Pipe::MGPipeHandle ElementBuffer = MG_Pipe::kMGPipeNullHandle; - // The bound GL_DRAW_INDIRECT_BUFFER and GL_PARAMETER_BUFFER, null when unbound. - MG_Pipe::MGPipeHandle DrawIndirectBuffer = MG_Pipe::kMGPipeNullHandle; - MG_Pipe::MGPipeHandle ParameterBuffer = MG_Pipe::kMGPipeNullHandle; - // GL_PRIMITIVE_RESTART / GL_PRIMITIVE_RESTART_FIXED_INDEX and the application's index, - // carried verbatim (informational in P5b: the backend reads its own barrier-pulled copy). - Bool PrimitiveRestart = false; - Uint32 RestartIndex = 0; - // P5e (vi), ID-82 / CONTRACT-P5E §5.1: TRUE when the bound VAO has at least one ENABLED - // attribute with no buffer object behind it, i.e. an array whose vertices live in the - // application's own memory. The server has no such memory: today it dereferences - // `attrib.Offset` as a raw client pointer from the apply thread - // (Managers.cpp's SyncClientSideAttributesForDrawArrays), which is legal only while the - // client is parked behind the record. It rides in MGPDrawInfo::Flags as kDrawClientArrays - // so BOTH roles can decide from the wire - the client refuses such a draw under - // run-ahead, and MGPipeBarriered's escalation (ii) keeps it barriered if one ever - // arrives anyway. Staging the bytes is P8's. - Bool ClientVertexArrays = false; - }; - - // 1 / 2 / 4 for the three GL index types, 0 for anything else (the frontend has already - // refused those with INVALID_ENUM before the slot is reached). - Uint8 RemoteIndexSizeFor(GLenum indexType); - - // The fixed head. `indexSize` 0 = arrays. `instanceCount` is the call's own (1 for a - // non-instanced entry point) and `baseInstance` its gl_BaseInstance value; the sink reads - // "instanced" as InstanceCount != 1 || StartInstance != 0, so an instanced call with a - // count of 1 and no base instance is dispatched as the plain draw it is equivalent to. - MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, - GLuint baseInstance, Uint32 numDraws, - const RemoteDrawBindings& bindings); - - // One MGPDrawRange from one (first | indices, count, basevertex). Arrays: Start = first. - // Indexed with an element buffer bound: Start = offset / IndexSize, and FALSE when the byte - // offset is not a whole number of indices or does not fit the record's Uint32 Start - the - // caller refuses by name rather than round. Indexed with no element buffer: Start = 0 and - // the bytes are the client's (the emitter stages `count * IndexSize` of them). - Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, - GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out); - - // The kDrawIsIndirect second tail: the bound GL_DRAW_INDIRECT_BUFFER handle, the call's - // `indirect` byte offset, the stride and the draw count; for the *IndirectCount forms the - // bound GL_PARAMETER_BUFFER handle and the byte offset the call spells as `drawcount`. - MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, - GLsizei drawCount, GLsizei stride, - GLintptr parameterOffset, Bool hasParameterBuffer); - // ---- P10 (CONTRACT-P10.md §1): fence polls answered from the reverse channel ---------- // // The event drain's half: the server reported `fence` signaled (kEventFenceSignaled). Every diff --git a/MobileGL/MG_Remote/Client/GpuWritePending.cpp b/MobileGL/MG_Remote/Client/GpuWritePending.cpp index 162665ab..52b1fe8c 100644 --- a/MobileGL/MG_Remote/Client/GpuWritePending.cpp +++ b/MobileGL/MG_Remote/Client/GpuWritePending.cpp @@ -26,165 +26,6 @@ namespace MobileGL::MG_Remote::Client { using MG_State::GLState::BufferObject; - using MobileGL::BufferTarget; - using MG_State::GLState::ImageTextureBinding; - - namespace { - // Per-row tallies. Diagnostics, and the only thing a unit case can assert on: an - // over-approximating set has no observable difference when a row fires too OFTEN, so - // "did this row fire at all, for this buffer" has to be readable directly. - Array(GpuWriteProducer::Count)> g_producerMarks{}; - - // The transform-feedback rows, shared by the draw walk (row 3) and by - // glEndTransformFeedback (row 5). Both mark the SAME set - the capture targets of the - // capture program - and the split exists only so the two can be counted apart. - void MarkTransformFeedbackTargets(GpuWriteProducer producer) { - auto& context = MG_State::pGLContext; - if (!context) return; - if (!context->IsTransformFeedbackActive()) return; - const auto& program = context->GetTransformFeedbackProgram(); - if (program == nullptr) return; - const SizeT declared = program->GetTransformFeedbackBufferCount(); - const SizeT count = - declared < MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS - ? declared - : static_cast(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS); - for (SizeT i = 0; i < count; ++i) { - const auto& point = - context->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); - MarkBufferForProducer(point.GetBoundObject(), producer); - } - } - - void MarkShaderStorageBindings() { - auto& context = MG_State::pGLContext; - if (!context) return; - // The TOUCHED count, exactly as the backend twin uses it - // (DirectGLES.cpp:559-560): the binding-point array is 84 entries wide and - // walking all of them on every draw is what the high-water mark exists to avoid. - const SizeT points = context->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage); - for (SizeT i = 0; i < points; ++i) { - const auto& point = context->GetBufferBindingPoint(BufferTarget::ShaderStorage, static_cast(i)); - MarkBufferForProducer(point.GetBoundObject(), GpuWriteProducer::ShaderStorageBinding); - } - } - - void MarkAtomicCounterBindings() { - auto& context = MG_State::pGLContext; - if (!context) return; - // WIDER THAN ITS BACKEND TWIN, ON PURPOSE AND IN THE SAFE DIRECTION. - // SyncAtomicCounterBuffers (DirectGLES.cpp:578-583) walks the GL bindings the - // TRANSPILED program declared, which is a subset of what is bound; the client has - // the bindings but not that per-program list at this point, so it marks every - // touched atomic-counter point. Over-approximating costs a readback the narrowing - // channel then removes. Under-approximating reads a stale counter and says - // nothing, which is the failure every atomic-counter conformance case is. - const SizeT points = context->GetTouchedBufferBindingPointCount(BufferTarget::AtomicCounter); - for (SizeT i = 0; i < points; ++i) { - const auto& point = context->GetBufferBindingPoint(BufferTarget::AtomicCounter, static_cast(i)); - MarkBufferForProducer(point.GetBoundObject(), GpuWriteProducer::AtomicCounterBinding); - } - } - - void MarkWritableImageBufferTextures() { - auto& context = MG_State::pGLContext; - if (!context) return; - // The backend keeps a bitset of writable image-buffer units - // (DirectGLES.cpp:2350-2352, maintained from its own SyncImageTextureBinding) and - // the client has no equivalent, so it sweeps. The sweep is NOT bounded by a device - // limit read: MaxImageUnits would be a backend read, and a stale or absent backend - // object would silently shorten the walk - which is the one direction this set may - // not fail in. - // - // IT IS BOUNDED BY THE FRONTEND's OWN IMAGE-UNIT HIGH-WATER MARK (P5d round 3, - // package C). The paragraph that stood here said the narrowing "belongs with P8's - // binding-walk migration"; the 2026-09-17 inproc profile moved it forward - - // MarkWritableImageBufferTextures was 2.15% self / 3.59% inclusive of the GL thread - // (GetImageTextureBinding 1.37 of it) at ~852 draws/frame, in a workload that never - // binds an image at all, purely because the walk was MAX_TEXTURE_IMAGE_UNITS (192) - // units wide unconditionally - 192 GetImageTextureBinding reads per draw for a - // context that has never bound one. - // - // AND THE MARK IS SAFE IN THE ONLY DIRECTION THAT MATTERS. It is written by - // glBindImageTexture itself (GL_Texture.cpp, TextureState::NoteImageUnitTouched) and - // it only ever grows - an unbind, a delete-unbind, a context that stops using an - // image unit all leave it where it was - so `unit <= mark` can only be WIDER than - // the set of units that hold a binding, never narrower. -1 means no image unit has - // ever been bound in this context, and then this is one compare. - const Int highest = context->GetMaxTouchedImageUnit(); - for (Int unit = 0; unit <= highest && unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; - ++unit) { - const auto& binding = context->GetImageTextureBinding(unit); - if (!ImageUnitIsAWritableBufferTexture(binding)) continue; - auto* textureBuffer = static_cast(binding.Texture.get()); - MarkBufferForProducer(textureBuffer->GetBufferBindingSlot().GetBoundObject(), - GpuWriteProducer::WritableImageBufferTexture); - } - } - } // namespace - - Bool GpuWriteSetIsClientSide() { - // P13 W4a: wherever the backend reads records - every wire, and monolith's record arm - - // the client owns the conservative set; the backend marks only its own twins. - return MG_Config::DataArmIsRecord(); - } - - Bool ImageUnitIsAWritableBufferTexture(const ImageTextureBinding& binding) { - // Verbatim from IsWritableImageBufferTexture (DirectGLES.cpp:2354-2357). All three - // terms are client state; none of them is a driver question. - return binding.Texture != nullptr && binding.Access != GL_READ_ONLY && - binding.Texture->GetStorageType() == TextureStorageType::Buffer; - } - - void MarkBufferForProducer(const SharedPtr& buffer, GpuWriteProducer producer) { - if (!GpuWriteSetIsClientSide()) return; - if (buffer == nullptr) return; - if (producer >= GpuWriteProducer::Count) return; - buffer->MarkGpuWritten(); - ++g_producerMarks[static_cast(producer)]; - } - - void MarkGpuWritesForDraw() { - if (!GpuWriteSetIsClientSide()) return; - MarkShaderStorageBindings(); - MarkAtomicCounterBindings(); - MarkWritableImageBufferTextures(); - MarkTransformFeedbackTargets(GpuWriteProducer::TransformFeedbackCapture); - } - - void MarkGpuWritesForDispatch() { - if (!GpuWriteSetIsClientSide()) return; - MarkShaderStorageBindings(); - MarkAtomicCounterBindings(); - MarkWritableImageBufferTextures(); - } - - void MarkReadPixelsPackBuffer() { - if (!GpuWriteSetIsClientSide()) return; - auto& context = MG_State::pGLContext; - if (!context) return; - MarkBufferForProducer(context->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(), - GpuWriteProducer::ReadPixelsPackBuffer); - } - - void MarkEndTransformFeedbackCaptureTargets() { - if (!GpuWriteSetIsClientSide()) return; - MarkTransformFeedbackTargets(GpuWriteProducer::EndTransformFeedbackCapture); - } - - Uint64 ProducerMarkCount(GpuWriteProducer producer) { - if (producer >= GpuWriteProducer::Count) return 0; - return g_producerMarks[static_cast(producer)]; - } - - void ResetProducerMarkCountsForTest() { - g_producerMarks.fill(0); - } - - Bool BufferWritebackIsReachable(const BufferObject& buffer) { - if (buffer.GetSize() == 0) return false; - return MG_Pipe::MGPipeResourceSubsystemEnabled(); - } SizeT BufferWritebackSliceBytes() { ClientSession* session = ClientSession::Active(); diff --git a/MobileGL/MG_Remote/Client/GpuWritePending.h b/MobileGL/MG_Remote/Client/GpuWritePending.h index 8b585218..da14ca18 100644 --- a/MobileGL/MG_Remote/Client/GpuWritePending.h +++ b/MobileGL/MG_Remote/Client/GpuWritePending.h @@ -6,56 +6,16 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// THE CLIENT-SIDE CONSERVATIVE GPU-WRITE SET (ARCHITECTURE.md:575 names this component; -// CONTRACT-P5.md section 3, table 2's first set). Owner: package b1. -// -// WHY IT HAS TO MOVE SIDES. `BufferObject::SyncGpuWrites` (BufferObject.cpp:369) runs -// synchronously, on the application's thread, the moment the application calls glMapBuffer or -// glGetBufferSubData. It has to answer "did the GPU write this buffer since I last read it?" -// - and today all six producers of that answer are BACKEND-side (DirectGLES.cpp:570, :618, -// :2603, UniformManager.cpp:1075, :1231, VulkanRenderer.cpp:11618), i.e. on the server's side -// of a split. Asking the server is a round trip the design forbids in steady state, so the -// client must build the set itself, from state it already owns: the SSBO and atomic-counter -// binding points, the image units whose Access is not GL_READ_ONLY, and the transform-feedback -// capture targets. -// -// CONSERVATIVE MEANS OVER-APPROXIMATE, AND THAT IS THE WHOLE DESIGN. The reverse channel's -// OnGpuWritten is a NARROWING channel (ResourceTracker.h:577-581): the client marks -// everything a shader COULD have written and the server only ever removes entries. So a row -// here that fires too often costs a readback; a row that fires too rarely reads a stale shadow -// and is silent. Every row below therefore mirrors its backend twin exactly, including the two -// places the twin is deliberately NARROW - a GL_READ_ONLY image binding is left alone, and the -// image walk runs from draw preparation rather than from glBindImageTexture's eager sync. -// -// NO NARROWING IN P5. ResourceTracker.h:587-592's `rangeCount == 1` assertion STAYS. Zero -// ranges will mean "a fully narrowed set - nothing is dirty" at P8/P9, and a package that -// reads ResourceTracker.h:577-581 alone will think it owns that already. It does not. -// -// THE TWO NEW PRODUCERS. Rows 4 and 5 have no backend twin: they are behaviour P5 ADDS -// (ARCHITECTURE.md:508), and both are strictly better than what monolith does. -// * glReadPixels into a pack PBO becomes fire-and-forget plus a client-side mark. Monolith -// maps the PBO and copies it into the shadow inside the call (DirectGLES.cpp:10983-10993), -// which is an unconditional stall on every glReadPixels whether or not anyone reads the -// shadow; marking instead defers the cost to the first read that actually wants it. -// * glEndTransformFeedback drops its unbounded ClientWaitSync (GL_Drawing.cpp:1367, timeout -// ~0ull) and marks the capture targets, for the same reason: the wait exists only so that -// a later MapBuffer sees real results, which is precisely what the flag is for. -// -// WHAT NO ROW COVERS, WRITTEN DOWN SO THE NEXT READER DOES NOT HAVE TO ASK. The set is built -// from the APPLICATION's bindings, so it says nothing about a backend's own scratch buffers - -// Espryt's converted-vertex-stream and primitive-restart substitution buffers, Magma's UBO -// ring. None of those has a MarkGpuWritten today either, so the client set is no NARROWER than -// monolith's and this is not a regression; it is a standing hole in both, and it stays one -// until the phase that migrates the backend's own allocations. -// -// GATED ON THE TRANSPORT, like everything else in this package: on the monolith path the six -// backend sites still run and a second marker would be new behaviour (D-J), and rows 4 and 5 -// would remove a stall monolith is entitled to keep. +// P13 W5: the session's half of the client GPU-write set - the writeback wait and the two +// clamps that depend on a live session's rings. The set itself (the producers, the walks, +// the row predicates) is MG_Record's (MG_Impl/Pipe/Verb/GpuWriteSet.h): the record arm +// keeps it in a library without a transport too. #pragma once #include #include +#include namespace MobileGL::MG_State::GLState { class BufferObject; @@ -64,77 +24,6 @@ namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_Remote::Client { - // ONE ROW PER PRODUCER, and the enum is the inventory: six that mirror a backend - // MarkGpuWritten site one-for-one, two that P5 adds. Each has exactly one unit case, and - // the per-row counters below are what those cases assert on - a row that stops firing is - // otherwise invisible, because an over-approximating set fails SILENTLY in the direction - // that matters. - enum class GpuWriteProducer : Uint8 { - // DirectGLES.cpp:570 (MarkShaderStorageBuffersGpuWritten) and - // UniformManager.cpp:1231 (ResolveStorageBufferDescriptor): every SSBO binding point, - // unconditional, once the points are bound and the draw or dispatch is going out. - ShaderStorageBinding = 0, - // DirectGLES.cpp:618 (SyncAtomicCounterBuffers): every bound atomic counter. The - // point of a counter is that the shader increments it and every conformance case - // reads the increment back with glMapBufferRange or glGetBufferSubData. - AtomicCounterBinding, - // DirectGLES.cpp:2603 (MarkWritableImageBufferTexturesGpuWritten) and - // UniformManager.cpp:1075 (ResolveStorageTexelBufferDescriptor): a buffer texture on - // an image unit, ONLY when Access != GL_READ_ONLY. Marking a read-only binding would - // make the next map wait on - and then re-read - a dispatch that could not have - // changed a byte of it. - WritableImageBufferTexture, - // VulkanRenderer.cpp:11618 (BeginXfbCaptureForDraw): the capture targets, because - // "the capture is a GPU write like any shader's". - TransformFeedbackCapture, - // P5, new: glReadPixels into a bound GL_PIXEL_PACK_BUFFER. - ReadPixelsPackBuffer, - // P5, new: glEndTransformFeedback, in place of the unbounded fence wait. - EndTransformFeedbackCapture, - Count - }; - - // Transport != Monolith. False means every entry point below is a no-op and the six - // backend sites are still the only producers, which is exactly today's behaviour. - Bool GpuWriteSetIsClientSide(); - - // ---- the walks ------------------------------------------------------------------- - // - // Called from the client's own draw / dispatch emission point, BEFORE the verb record - // goes out, for the same ordering reason the persistent-map push has: the set must - // describe the work the record is about to start. - - // Rows 0, 1, 2 and 3. - void MarkGpuWritesForDraw(); - // Rows 0, 1 and 2. A dispatch has no transform feedback. - void MarkGpuWritesForDispatch(); - - // ---- the two new producers ------------------------------------------------------- - - // Row 4. Marks whatever is bound to GL_PIXEL_PACK_BUFFER, or nothing when the read goes - // to client memory - which is the case the backend's map-and-copy never had to consider, - // because it only ran when a PBO was bound in the first place. - void MarkReadPixelsPackBuffer(); - // Row 5. Must be called while the capture state is still ACTIVE: GLContext's - // EndTransformFeedback clears the live bindings, so a mark taken after it marks nothing. - void MarkEndTransformFeedbackCaptureTargets(); - - // ---- the row predicates, exposed so a unit case can drive one row at a time ------- - - // Row 2's discriminator, verbatim from DirectGLES.cpp:2354-2357. It is a function rather - // than three inline conditions at the call site because it is the one row whose backend - // twin is narrow on purpose, and a client that re-derived it slightly wider would mark - // read-only image bindings with nothing able to see that it had. - Bool ImageUnitIsAWritableBufferTexture(const MG_State::GLState::ImageTextureBinding& binding); - - // The one place a row actually marks. Null and duplicate marks are absorbed here so the - // walks stay readable, and the per-row counter moves only when a buffer really was - // marked. - void MarkBufferForProducer(const SharedPtr& buffer, - GpuWriteProducer producer); - - Uint64 ProducerMarkCount(GpuWriteProducer producer); - void ResetProducerMarkCountsForTest(); // ---- SyncGpuWrites' third state (CONTRACT-P5.md section 3) ----------------------- @@ -144,10 +33,6 @@ namespace MobileGL::MG_Remote::Client { // caller writes, because the loop would be a hang in exactly that configuration. void AwaitBufferWriteback(MG_State::GLState::BufferObject& buffer); - // Is there a readback route at all? A buffer with no size, or one whose backend registered - // no resource ops, can never catch up, and SyncGpuWrites must clear rather than block for - // ever. It is the ONE case monolith's unconditional clear covers that a writeback cannot. - Bool BufferWritebackIsReachable(const MG_State::GLState::BufferObject& buffer); // The slice a whole-buffer readback is cut into so one writeback event always fits // SEG_EVENT. The writeback's bytes travel INLINE in the event record (P5c ev, CONTRACT-P5C diff --git a/MobileGL/MG_Remote/Client/QueryEmit.inc b/MobileGL/MG_Remote/Client/QueryEmit.inc index 96db610d..f0f78db6 100644 --- a/MobileGL/MG_Remote/Client/QueryEmit.inc +++ b/MobileGL/MG_Remote/Client/QueryEmit.inc @@ -127,16 +127,4 @@ return ReadQueryReply(session, MG_Pipe::MGPWireOp::QueryTimestamp, &request, sizeof(request)); } - void EmitDeleteTransformFeedback(GLuint name) { - VerbChannel session = ChannelFor("DeleteTransformFeedback"); - BeforeReadOnlyVerb(); - const auto& context = *MG_State::pGLContext; - const MG_Pipe::MGPStreamOutputBind object{name, 0, context.GetTransformFeedbackLifetimeId(name)}; - session.EmitAndWait(MG_Pipe::MGPWireOp::DeleteStreamOutput, &object, sizeof(object), - nullptr, 0, nullptr, 0, nullptr); - if (context.GetBoundTransformFeedbackName() == name) { - const MG_Pipe::MGPStreamOutputBind fallback{0, 0, context.GetTransformFeedbackLifetimeId(0)}; - session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &fallback, sizeof(fallback), - nullptr, 0, nullptr, 0, nullptr); - } } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index cbfa395a..10e43f21 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -630,7 +630,7 @@ namespace MobileGL::MG_State::GLState { // resource ops - can never catch up, and retrying on every subsequent read would // only repeat the same no-op. That is the ONE case the monolith clear covers that // the writeback cannot, so it is spelled out here rather than inherited. - if (m_gpuWritePending && !MG_Remote::Client::BufferWritebackIsReachable(*this)) { + if (m_gpuWritePending && !MG_Record::BufferWritebackIsReachable(*this)) { m_gpuWritePending = false; } return; diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index 8d82c48f..a6e101ac 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -38,7 +38,7 @@ namespace { #if MOBILEGL_BUILD_DISAGGREGATED using MG_State::GLState::BufferObject; - using MG_Remote::Client::GpuWriteProducer; + using MG_Record::GpuWriteProducer; using MG_Record::PersistentMapTracker; // A backend that MINTS a persistent mapping, for the one case that needs the adopted arm @@ -76,7 +76,7 @@ namespace { MG_Config::Features.PipeStats = true; MG_Util::PipeStats::Init(); PersistentMapTracker::Instance().ClearForTest(); - MG_Remote::Client::ResetProducerMarkCountsForTest(); + MG_Record::ResetProducerMarkCountsForTest(); } void TearDown() override { // Belt and braces for the one case that installs a minting backend: a table left @@ -128,9 +128,9 @@ TEST_F(SplitBufferSet, Row0EverySsboBindingPointIsMarkedByADraw) { MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0).Bind(ssbo); MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::ShaderStorage, 0); - MG_Remote::Client::MarkGpuWritesForDraw(); + MG_Record::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ShaderStorageBinding), 1u) + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::ShaderStorageBinding), 1u) << "a draw with an SSBO bound must mark it: the shader writes into the driver's buffer, " "behind the shadow glMapBuffer and glGetBufferSubData read"; } @@ -141,9 +141,9 @@ TEST_F(SplitBufferSet, Row1EveryBoundAtomicCounterIsMarkedByADraw) { MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter, 0).Bind(counter); MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::AtomicCounter, 0); - MG_Remote::Client::MarkGpuWritesForDraw(); + MG_Record::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::AtomicCounterBinding), 1u); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::AtomicCounterBinding), 1u); } // Row 2's DISCRIMINATOR - DirectGLES.cpp:2354-2357. This is the one row whose backend twin is @@ -152,7 +152,7 @@ TEST_F(SplitBufferSet, Row1EveryBoundAtomicCounterIsMarkedByADraw) { // a byte of it. TEST_F(SplitBufferSet, Row2OnlyAWritableImageBufferTextureCounts) { MG_State::GLState::ImageTextureBinding empty{}; - EXPECT_FALSE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(empty)) + EXPECT_FALSE(MG_Record::ImageUnitIsAWritableBufferTexture(empty)) << "an unbound image unit is not a GPU write"; auto texture = MakeShared(7u); @@ -162,13 +162,13 @@ TEST_F(SplitBufferSet, Row2OnlyAWritableImageBufferTextureCounts) { MG_State::GLState::ImageTextureBinding readOnly{}; readOnly.Texture = texture; readOnly.Access = GL_READ_ONLY; - EXPECT_FALSE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(readOnly)) + EXPECT_FALSE(MG_Record::ImageUnitIsAWritableBufferTexture(readOnly)) << "a GL_READ_ONLY image binding is left alone by the backend twin and must be left " "alone here"; MG_State::GLState::ImageTextureBinding writable = readOnly; writable.Access = GL_READ_WRITE; - EXPECT_TRUE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(writable)); + EXPECT_TRUE(MG_Record::ImageUnitIsAWritableBufferTexture(writable)); } // Row 2's WALK, through the image unit the context actually holds. @@ -186,9 +186,9 @@ TEST_F(SplitBufferSet, Row2AWritableImageBufferTextureIsMarkedByADraw) { binding.Access = GL_WRITE_ONLY; MG_State::pGLContext->NoteImageUnitTouched(0); - MG_Remote::Client::MarkGpuWritesForDraw(); + MG_Record::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 1u); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 1u); binding = MG_State::GLState::ImageTextureBinding{}; } @@ -216,13 +216,13 @@ TEST_F(SplitBufferSet, Row2TheSweepIsBoundedByTheImageUnitHighWaterMark) { binding.Texture = texture; binding.Access = GL_READ_WRITE; - MG_Remote::Client::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 0u) + MG_Record::MarkGpuWritesForDraw(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 0u) << "the mark has never moved, so the sweep must not walk a single unit"; MG_State::pGLContext->NoteImageUnitTouched(kUnit); - MG_Remote::Client::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 1u) + MG_Record::MarkGpuWritesForDraw(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 1u) << "a unit at the mark is inside the walk"; binding = MG_State::GLState::ImageTextureBinding{}; @@ -237,35 +237,35 @@ TEST_F(SplitBufferSet, Row3TransformFeedbackTargetsAreOnlyMarkedWhileACaptureIsA MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::TransformFeedback, 0); ASSERT_FALSE(MG_State::pGLContext->IsTransformFeedbackActive()); - MG_Remote::Client::MarkGpuWritesForDraw(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 0u); + MG_Record::MarkGpuWritesForDraw(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 0u); // The marking itself, driven at the row rather than through the capture state machine. - MG_Remote::Client::MarkBufferForProducer(target, GpuWriteProducer::TransformFeedbackCapture); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 1u); + MG_Record::MarkBufferForProducer(target, GpuWriteProducer::TransformFeedbackCapture); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 1u); } // Row 4 - P5's own: glReadPixels into a bound GL_PIXEL_PACK_BUFFER. TEST_F(SplitBufferSet, Row4AReadPixelsIntoAPackPboMarksThePbo) { - MG_Remote::Client::MarkReadPixelsPackBuffer(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 0u) + MG_Record::MarkReadPixelsPackBuffer(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 0u) << "a read into client memory binds no PBO and must mark nothing"; auto pbo = MakeBuffer(16u, 1024); MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).Bind(pbo); - MG_Remote::Client::MarkReadPixelsPackBuffer(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 1u); + MG_Record::MarkReadPixelsPackBuffer(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 1u); } // Row 5 - P5's own: glEndTransformFeedback, in place of the unbounded ClientWaitSync. TEST_F(SplitBufferSet, Row5EndTransformFeedbackMarksTheCaptureTargets) { auto target = MakeBuffer(17u, 256); ASSERT_FALSE(MG_State::pGLContext->IsTransformFeedbackActive()); - MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets(); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 0u); + MG_Record::MarkEndTransformFeedbackCaptureTargets(); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 0u); - MG_Remote::Client::MarkBufferForProducer(target, GpuWriteProducer::EndTransformFeedbackCapture); - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 1u); + MG_Record::MarkBufferForProducer(target, GpuWriteProducer::EndTransformFeedbackCapture); + EXPECT_EQ(MG_Record::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 1u); } // THE GATE ITSELF. On the monolith path the six backend sites are still the only producers and @@ -277,13 +277,13 @@ TEST_F(SplitBufferSet, TheWholeSetIsInertOnTheMonolithPath) { MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0).Bind(ssbo); MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::ShaderStorage, 0); - MG_Remote::Client::MarkGpuWritesForDraw(); - MG_Remote::Client::MarkGpuWritesForDispatch(); - MG_Remote::Client::MarkReadPixelsPackBuffer(); - MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets(); + MG_Record::MarkGpuWritesForDraw(); + MG_Record::MarkGpuWritesForDispatch(); + MG_Record::MarkReadPixelsPackBuffer(); + MG_Record::MarkEndTransformFeedbackCaptureTargets(); for (SizeT row = 0; row < static_cast(GpuWriteProducer::Count); ++row) { - EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(static_cast(row)), 0u) + EXPECT_EQ(MG_Record::ProducerMarkCount(static_cast(row)), 0u) << "row " << row << " fired with Transport == Monolith"; } } @@ -532,7 +532,7 @@ TEST_F(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheReques // And with no readback route at all - no size, or a backend that registered no resource // ops - SyncGpuWrites must clear rather than block for ever. That is the ONE case // monolith's unconditional clear covers that a writeback cannot. - EXPECT_FALSE(MG_Remote::Client::BufferWritebackIsReachable(*buffer)); + EXPECT_FALSE(MG_Record::BufferWritebackIsReachable(*buffer)); buffer->SyncGpuWrites(); EXPECT_FALSE(buffer->HasOutstandingGpuWrite()); }