mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Feat] (MG_Remote, Client): the conservative GPU-write set and the persistent-map block tracker, on the client, where the decision SyncGpuWrites makes synchronously actually lives
This commit is contained in:
@@ -571,6 +571,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||||
|
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
||||||
|
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||||
|
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||||
|
MobileGL/MG_Remote/Client/GpuWritePending.cpp
|
||||||
|
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
|
||||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/GpuWritePending.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
|
||||||
|
|
||||||
|
#include "GpuWritePending.h"
|
||||||
|
|
||||||
|
#include "ClientSession.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_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 bounded by the array,
|
||||||
|
// not 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. The loop body is a null test on a
|
||||||
|
// contiguous array until a unit is actually bound. P5 records cost and does not
|
||||||
|
// gate on it (2026-09-08 rule); an image-unit high-water mark on GLContext is the
|
||||||
|
// obvious narrowing and belongs with P8's binding-walk migration.
|
||||||
|
for (Int unit = 0; 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() {
|
||||||
|
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
return MG_Pipe::MGPipeResourceSubsystemEnabled();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void AwaitBufferWriteback(BufferObject& buffer) {
|
||||||
|
(void)buffer;
|
||||||
|
// THE WAIT IS THE BARRIER'S WAIT (R-3). The reply-slot id IS the record's seq, so
|
||||||
|
// "appliedSeq reached my readback" and "my answer is back" are one condition, and
|
||||||
|
// ClientSession::EmitAndWait has already paid for it by the time the emitter returns.
|
||||||
|
// With no session - a build-split lane running monolith, and every unit case - the
|
||||||
|
// emission WAS the application, synchronously, so the writeback has already landed
|
||||||
|
// and there is nothing to wait for. Spelling that as "return" rather than as a loop
|
||||||
|
// is deliberate: a loop here would be a hang in exactly that configuration, which is
|
||||||
|
// the configuration every gate lane runs.
|
||||||
|
if (ClientSession::Active() == nullptr) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/PersistentMapTracker.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
|
||||||
|
|
||||||
|
#include "PersistentMapTracker.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
using MG_State::GLState::BufferObject;
|
||||||
|
using MobileGL::BufferMappingAccessBit;
|
||||||
|
|
||||||
|
PersistentMapTracker& PersistentMapTracker::Instance() {
|
||||||
|
// Leaked on purpose, once, like every other role-local singleton (ID-8): a buffer's
|
||||||
|
// destructor runs from exit handlers after this TU's globals would already be gone,
|
||||||
|
// and it calls Forget().
|
||||||
|
static PersistentMapTracker* instance = new PersistentMapTracker{};
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 PersistentMapTracker::BlockBytes() {
|
||||||
|
return static_cast<Uint64>(MG_Config::Ipc.PersistentBlockKb) * 1024ull;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool PersistentMapTracker::PushIsArmed() {
|
||||||
|
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncPersistentMappedRange's early-out chain (BufferObject.cpp:341-353), in its order,
|
||||||
|
// read as a membership test. Every line here has a line there; if one of them moves, the
|
||||||
|
// unit case that drives both against each other is what says so.
|
||||||
|
Bool PersistentMapTracker::IsLivePersistentMap(const BufferObject& buffer) {
|
||||||
|
if (!buffer.IsMapped()) return false;
|
||||||
|
// GPU-resident: the application already wrote into coherent GPU memory and there is
|
||||||
|
// nothing to ship. At tier T2 this arm is unreachable - MapPersistent declines - but
|
||||||
|
// the predicate must still read the chain, not the tier: a build that reaches T0/T1
|
||||||
|
// later must see this row answer for itself.
|
||||||
|
if (buffer.IsBackendPersistentMapped()) return false;
|
||||||
|
const auto access = buffer.GetMappingAccess();
|
||||||
|
if (!(access & BufferMappingAccessBit::Persistent)) return false;
|
||||||
|
if (!(access & BufferMappingAccessBit::Write)) return false;
|
||||||
|
// FLUSH_EXPLICIT: the application promises to announce its own writes with
|
||||||
|
// glFlushMappedBufferRange, which already crosses as resource_flush_range. Pushing
|
||||||
|
// here as well would ship the same bytes twice and take the upload-shape decision
|
||||||
|
// away from the side that pays for it.
|
||||||
|
if (access & BufferMappingAccessBit::FlushExplicit) return false;
|
||||||
|
const auto range = buffer.GetMappedRange();
|
||||||
|
if (range.start >= range.end) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentMapTracker::NoteMapStateChanged(BufferObject& buffer) {
|
||||||
|
const Uint64 key = buffer.GetLifetimeId();
|
||||||
|
if (IsLivePersistentMap(buffer)) {
|
||||||
|
m_livePersistentMaps[key] = &buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_livePersistentMaps.erase(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentMapTracker::Forget(const BufferObject& buffer) {
|
||||||
|
m_livePersistentMaps.erase(buffer.GetLifetimeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentMapTracker::PushBlocksFor(BufferObject& buffer) {
|
||||||
|
if (!PushIsArmed()) return;
|
||||||
|
// Re-checked rather than trusted. The set is maintained at five events and a sixth
|
||||||
|
// one arriving without a NoteMapStateChanged would otherwise push a buffer whose
|
||||||
|
// shadow has been released - an adopted store's Bytes() is the GPU map, and reading
|
||||||
|
// it as if it were the shadow is how a "conservative" push turns into a fault.
|
||||||
|
if (!IsLivePersistentMap(buffer)) {
|
||||||
|
Forget(buffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Uint64 blockBytes = BlockBytes();
|
||||||
|
// 0 IS THE NEGATIVE CONTROL, NOT "unlimited" (E3(a)). Pushing one whole-span block
|
||||||
|
// here would make the control green for the wrong reason - it has to disable the
|
||||||
|
// push, so that PersistentCoherentMapScenario draws the last uploaded bytes and goes
|
||||||
|
// red exactly the way an unpushed map does.
|
||||||
|
if (blockBytes == 0) return;
|
||||||
|
|
||||||
|
const auto range = buffer.GetMappedRange();
|
||||||
|
const Uint64 begin = static_cast<Uint64>(range.start);
|
||||||
|
const Uint64 end = static_cast<Uint64>(range.end);
|
||||||
|
for (Uint64 at = begin; at < end; at += blockBytes) {
|
||||||
|
const Uint64 length = (end - at) < blockBytes ? (end - at) : blockBytes;
|
||||||
|
buffer.PushMappedSpanBlock(static_cast<SizeT>(at), static_cast<SizeT>(length));
|
||||||
|
++m_blocksPushed;
|
||||||
|
m_bytesPushed += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentMapTracker::PushAllMembers() {
|
||||||
|
if (!PushIsArmed()) return;
|
||||||
|
if (m_livePersistentMaps.empty()) return;
|
||||||
|
// Copied out first: PushBlocksFor can erase its own entry (a member that stopped
|
||||||
|
// being one), and ska::flat_hash_map invalidates on erase.
|
||||||
|
Vector<BufferObject*> members;
|
||||||
|
members.reserve(m_livePersistentMaps.size());
|
||||||
|
for (const auto& entry : m_livePersistentMaps) members.push_back(entry.second);
|
||||||
|
for (BufferObject* buffer : members) {
|
||||||
|
if (buffer != nullptr) PushBlocksFor(*buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushPersistentMapsBeforeVerb() {
|
||||||
|
PersistentMapTracker::Instance().PushAllMembers();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool AdoptTierIsEmulate() {
|
||||||
|
const Uint32 tier = MG_Config::Ipc.AdoptTier;
|
||||||
|
if (tier == 2) return true;
|
||||||
|
// A NAMED refusal, not a silent fall back to T2. T0 (a real cross-process shared
|
||||||
|
// mapping) and T1 (a server-side staging map) are P11's, and the reason the knob
|
||||||
|
// parses them today is that the negative control needs a spelling before the thing
|
||||||
|
// it controls exists. Falling back would make `MOBILEGL_IPC_ADOPT_TIER=0` look like
|
||||||
|
// a working T0 run and silently produce pmap bytes it must not produce.
|
||||||
|
MGLOG_F("MGPipe: MOBILEGL_IPC_ADOPT_TIER=%u names an adoption tier P11 implements and P5 "
|
||||||
|
"does not; P5 runs at T2 (emulate) only.",
|
||||||
|
static_cast<unsigned>(tier));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/PersistentMapTracker.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 BLOCK-GRANULARITY PERSISTENT-MAP PUSH (P5 R-6, CONTRACT-P5.md section 3, table 2's
|
||||||
|
// second set). Owner: package b1.
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS AT ALL. A coherent persistent map is the one buffer shape with no per-write
|
||||||
|
// API call: the application memcpys through the pointer and neither a serial, an epoch nor a
|
||||||
|
// record moves. In monolith that is free, because the pointer IS the backend's GPU storage -
|
||||||
|
// MEASUREMENTS.md:87 prices the adoption at p99 163 -> 21 ms and ~400 MB saved, and
|
||||||
|
// ARCHITECTURE.md:481 requires it to hold unmoved for the whole monolith track. Across a
|
||||||
|
// process boundary the adopted address is meaningless, so P5 runs at tier T2 (emulate): the
|
||||||
|
// client keeps the shadow, MGPipeApplyMapPersistent declines, and the bytes the application
|
||||||
|
// wrote with no call have to be SHIPPED. `persistent-map-push` (PipeStats `pmap`) is exactly
|
||||||
|
// those bytes, and it is structurally zero while adoption survives - which is why forcing T2
|
||||||
|
// and wiring the counter are one deliverable and not two.
|
||||||
|
//
|
||||||
|
// THE SET. `m_livePersistentMaps` is SyncPersistentMappedRange's own early-out chain
|
||||||
|
// (BufferObject.cpp:341-353) read as a membership test - mapped, NOT GPU-resident, Persistent,
|
||||||
|
// Write, NOT FlushExplicit, non-empty mapped range - and nothing else. Reading it as a
|
||||||
|
// predicate rather than re-deriving one is deliberate: the day that chain grows a sixth
|
||||||
|
// early-out, a re-derived predicate silently keeps pushing a buffer the monolith path stopped
|
||||||
|
// pushing, and the two arms diverge with no test able to say so. IsLivePersistentMap() is the
|
||||||
|
// single spelling; BufferObject::SyncPersistentMappedRange is held to it by a unit case.
|
||||||
|
//
|
||||||
|
// THE GRANULARITY. MOBILEGL_IPC_PERSISTENT_BLOCK_KB (default 64) blocks, keyed
|
||||||
|
// {MGPipeHandle, blockIndex} - the key is implicit, because a block ships as an ORDINARY
|
||||||
|
// resource_subdata record whose destination range is [blockIndex * blockBytes, + blockBytes).
|
||||||
|
// NO NEW RECORD KIND: the existing record is already chunked by
|
||||||
|
// MGPipeForEachSubDataRecordRange and already acceptance-gated, and a second way to say
|
||||||
|
// "these bytes go there" is a second way to get it wrong. A block size of 0 is E3(a)'s
|
||||||
|
// NEGATIVE CONTROL and means "push nothing" - not "one unlimited block" - so
|
||||||
|
// PersistentCoherentMapScenario must go red under it.
|
||||||
|
//
|
||||||
|
// PHASE 1 IS CONSERVATIVE, AND SAYS SO. The whole mapped span is pushed, by block, at every
|
||||||
|
// validate point; no dirty bits, no memcmp. Phase 2 (MOBILEGL_IPC_SHADOW_SHM, P6+) makes it
|
||||||
|
// precise, and ARCHITECTURE.md:499 grants it the right to be pulled forward if Phase 1 is
|
||||||
|
// unacceptable on the Create/Flywheel fixtures - the one place in the plan where a
|
||||||
|
// measurement may reorder phases.
|
||||||
|
//
|
||||||
|
// ORDERING IS THE CORRECTNESS PROPERTY, NOT THE GRANULARITY. In monolith the push is a memcpy
|
||||||
|
// on the same thread as the draw that follows it, so the bytes the application wrote before
|
||||||
|
// the draw are the bytes the draw sees. Under split both travel SEG_CMD in order, so the ring
|
||||||
|
// preserves it - PROVIDED the push is emitted AT the validate point and not lazily. A push
|
||||||
|
// deferred past its own draw record is the C-1 regression re-committed at the transport layer.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class BufferObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
class PersistentMapTracker {
|
||||||
|
public:
|
||||||
|
// Client-role singleton (table 3: the MG_Impl/MG_Remote/Client singletons are
|
||||||
|
// client-exclusive). Leaks at exit for ID-8's reason, once per role-local singleton.
|
||||||
|
static PersistentMapTracker& Instance();
|
||||||
|
|
||||||
|
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB * 1024. Zero means the push is OFF (E3(a)).
|
||||||
|
static Uint64 BlockBytes();
|
||||||
|
// Transport != Monolith. The whole module is inert on the monolith path: an extra
|
||||||
|
// resource_subdata record there would be new behaviour, which D-J forbids.
|
||||||
|
static Bool PushIsArmed();
|
||||||
|
|
||||||
|
// SyncPersistentMappedRange's early-out chain as a predicate. THE only spelling.
|
||||||
|
static Bool IsLivePersistentMap(const MG_State::GLState::BufferObject& buffer);
|
||||||
|
|
||||||
|
// Membership maintenance, both idempotent and both safe to call on a buffer that is
|
||||||
|
// not a member. Called from BufferObject on every event that can move the predicate:
|
||||||
|
// map, unmap, respecify, adoption, destruction.
|
||||||
|
void NoteMapStateChanged(MG_State::GLState::BufferObject& buffer);
|
||||||
|
void Forget(const MG_State::GLState::BufferObject& buffer);
|
||||||
|
|
||||||
|
// One member's whole mapped span, by block. Re-checks the predicate first, so a
|
||||||
|
// member that stopped being one (an adoption, an unmap that did not route through
|
||||||
|
// NoteMapStateChanged) is dropped rather than pushed.
|
||||||
|
void PushBlocksFor(MG_State::GLState::BufferObject& buffer);
|
||||||
|
|
||||||
|
// THE VALIDATE-POINT HOOK. Every member, before the verb record is emitted.
|
||||||
|
void PushAllMembers();
|
||||||
|
|
||||||
|
SizeT MemberCount() const { return m_livePersistentMaps.size(); }
|
||||||
|
Uint64 BlocksPushed() const { return m_blocksPushed; }
|
||||||
|
Uint64 BytesPushed() const { return m_bytesPushed; }
|
||||||
|
// Unit tests only: the counters are diagnostics, the set is not reset by it.
|
||||||
|
void ResetCountersForTest() {
|
||||||
|
m_blocksPushed = 0;
|
||||||
|
m_bytesPushed = 0;
|
||||||
|
}
|
||||||
|
void ClearForTest() {
|
||||||
|
m_livePersistentMaps.clear();
|
||||||
|
ResetCountersForTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Keyed on BufferObject::GetLifetimeId(), which is globally unique and never reused -
|
||||||
|
// never the GL name (LIFO-recycled by glGenBuffers) and never the heap address
|
||||||
|
// (recycled by the allocator). The raw pointer is safe because every removal path is
|
||||||
|
// explicit: ~BufferObject and ReleaseMemory both call Forget/NoteMapStateChanged, and
|
||||||
|
// PushBlocksFor re-checks the predicate before it dereferences anything it kept.
|
||||||
|
UnorderedMap<Uint64, MG_State::GLState::BufferObject*> m_livePersistentMaps;
|
||||||
|
Uint64 m_blocksPushed = 0;
|
||||||
|
Uint64 m_bytesPushed = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the client's emit table calls immediately BEFORE emitting any verb that can read a
|
||||||
|
// buffer (draw, dispatch, readback, blit, present). It is a free function rather than a
|
||||||
|
// method so the emit table does not have to name the singleton, and so the one-line call
|
||||||
|
// reads as what it is: "publish everything the application wrote with no call".
|
||||||
|
//
|
||||||
|
// In P5 the 21 SyncPersistentMappedRange sites (CONTRACT-P5.md section 3: 9 Espryt + 12
|
||||||
|
// Magma, MEASUREMENTS.md:111's 20 being one low) still stand where they are and route
|
||||||
|
// into PushBlocksFor through BufferObject::SyncPersistentMappedRange, so the push already
|
||||||
|
// happens at every point monolith pushes at. They retire into THIS call at P8, when the
|
||||||
|
// draw-path binding walks move to the client.
|
||||||
|
void PushPersistentMapsBeforeVerb();
|
||||||
|
|
||||||
|
// R-6's tier gate, and the ONE spelling of it. True for MOBILEGL_IPC_ADOPT_TIER=2, the
|
||||||
|
// only tier P5 implements; 0 (a real cross-process shared mapping) and 1 (a server-side
|
||||||
|
// staging map) parse - so the negative control has a name before the thing it controls
|
||||||
|
// exists - and are a NAMED refusal here rather than a silent fall back to T2. It is asked
|
||||||
|
// by MGPipeApplyMapPersistent, which is where the decline is decided, so the client's
|
||||||
|
// three adoption call sites keep their existing "null means declined" branch and the
|
||||||
|
// map-persistent-roundtrips counter keeps counting ATTEMPTS in both arms (E3(c) asserts
|
||||||
|
// mpr is equal between the monolith and the split arm, which is only true if the decline
|
||||||
|
// happens after the count, on the applier's side of the emission).
|
||||||
|
Bool AdoptTierIsEmulate();
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
Reference in New Issue
Block a user