[Refactor] (P13 W5 S4c): the verb emitters, the monolith verb port and the client GPU-write set move to MG_Record under MG_Impl/Pipe/Verb.

This commit is contained in:
2026-10-06 12:51:02 -04:00
parent ed7800a550
commit 8e76f9dcb7
19 changed files with 2700 additions and 2405 deletions
+4
View File
@@ -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
)
+6 -4
View File
@@ -24,8 +24,10 @@
#if MOBILEGL_PIPE_VERIFY
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#if MOBILEGL_BUILD_RECORD_ARM
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/ServerProbe.h>
#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
}
@@ -12,8 +12,8 @@
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/GpuWritePending.h>
#if MOBILEGL_BUILD_RECORD_ARM
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
#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
+182
View File
@@ -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 <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_State/GLState/TextureState/TextureState.h>
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<Uint64, static_cast<SizeT>(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<SizeT>(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
for (SizeT i = 0; i < count; ++i) {
const auto& point =
context->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast<Uint>(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<Uint>(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<Uint>(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<MG_State::GLState::TextureObjectBuffer*>(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<BufferObject>& buffer, GpuWriteProducer producer) {
if (!GpuWriteSetIsClientSide()) return;
if (buffer == nullptr) return;
if (producer >= GpuWriteProducer::Count) return;
buffer->MarkGpuWritten();
++g_producerMarks[static_cast<SizeT>(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<SizeT>(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
+147
View File
@@ -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 <Includes.h>
#include <Config.h>
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<MG_State::GLState::BufferObject>& 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
@@ -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<Uint8> bytes(static_cast<SizeT>(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);
File diff suppressed because it is too large Load Diff
+255
View File
@@ -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 <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_Pipe/PipeVerbSink.h>
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 <typename Visit>
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
@@ -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;
+7
View File
@@ -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
// ----------------------------------------------------------------------------
+6
View File
@@ -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
@@ -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
File diff suppressed because it is too large Load Diff
+26 -193
View File
@@ -66,6 +66,7 @@
// the dependency the aliases have explicit.
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_Impl/Pipe/Verb/VerbPort.h>
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 <typename Visit>
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
@@ -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<Uint64, static_cast<SizeT>(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<SizeT>(MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
for (SizeT i = 0; i < count; ++i) {
const auto& point =
context->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast<Uint>(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<Uint>(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<Uint>(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<MG_State::GLState::TextureObjectBuffer*>(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<BufferObject>& buffer, GpuWriteProducer producer) {
if (!GpuWriteSetIsClientSide()) return;
if (buffer == nullptr) return;
if (producer >= GpuWriteProducer::Count) return;
buffer->MarkGpuWritten();
++g_producerMarks[static_cast<SizeT>(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<SizeT>(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();
+5 -120
View File
@@ -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 <Includes.h>
#include <Config.h>
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
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<MG_State::GLState::BufferObject>& 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
-12
View File
@@ -127,16 +127,4 @@
return ReadQueryReply<Int64>(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);
}
}
@@ -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;
+33 -33
View File
@@ -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<MG_State::GLState::TextureObjectBuffer>(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<SizeT>(GpuWriteProducer::Count); ++row) {
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(static_cast<GpuWriteProducer>(row)), 0u)
EXPECT_EQ(MG_Record::ProducerMarkCount(static_cast<GpuWriteProducer>(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());
}