mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+3
-3
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user