Files
MobileGL/MobileGL/MG_Remote/Client/GpuWritePending.h
T

153 lines
8.9 KiB
C++

// MobileGL - MobileGL/MG_Remote/Client/GpuWritePending.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.
#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_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) -----------------------
// Blocks until this buffer's OnBufferWriteback has landed. With no session - a build-split
// lane running monolith, and every unit case - the emission was synchronous and the answer
// is already in, so this returns at once; that is why it is a call rather than a loop the
// 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);
} // namespace MobileGL::MG_Remote::Client