mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 05:38:31 +09:00
[Feat] (Pipe, State): mint a {slot, gen} handle for every buffer object and publish its create, respecify, sub-data, flush, readback and destroy as pipe calls
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/CsoCache.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
@@ -542,6 +543,171 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P3a: the resource family's emission (brief D-A, D-B, D-C, D-D)
|
||||
// ================================================================================
|
||||
//
|
||||
// Declared in MG_Pipe/PipeMutation.h and defined here for the layering reason that
|
||||
// header states: the emission sites are BufferObject's dispatchers, which are MG_State's,
|
||||
// and MG_State may see a declaration but never MG_Impl/Pipe/ResourceTracker.h.
|
||||
//
|
||||
// Every one of these is called from a site that has ALREADY asked
|
||||
// MGPipeResourceSubsystemEnabled(), except the mint and the destroy - the handle is
|
||||
// client state and its lifetime is the frontend object's, not the subsystem's.
|
||||
namespace {
|
||||
using MG_State::GLState::BufferObject;
|
||||
|
||||
MGPHandleOnly BufferHandleOnly(MGPipeHandle handle) {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::Buffer);
|
||||
return only;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeResourceSubsystemEnabled() {
|
||||
return (MG_Config::Features.PipePush & kMGPipeSubsystemResources) != 0 &&
|
||||
MGPipeGetResourceOps() != nullptr;
|
||||
}
|
||||
|
||||
Bool MGPipeResourceOpsHaveSubDataResident() {
|
||||
const MGPipeResourceOps* ops = MGPipeGetResourceOps();
|
||||
return ops != nullptr && ops->SubDataResident != nullptr;
|
||||
}
|
||||
|
||||
void MGPipeMintResourceHandle(BufferObject& buffer) {
|
||||
// UNCONDITIONAL in a push build, deliberately: set_vertex_buffers names a buffer by
|
||||
// handle whether or not the resource family is switched on, so gating the mint on
|
||||
// the resource subsystem bit would make the vertex-input subsystem emit null handles
|
||||
// in exactly the A/B arm that exists to isolate the two. It costs one free-list pop
|
||||
// and one map insert per buffer object and emits nothing.
|
||||
MGPipeInstallClientResourceCallbacks();
|
||||
MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceCreate(BufferObject& buffer) {
|
||||
MGPipeResourceTracker& tracker = MGPipeResourceTrackerInstance();
|
||||
const MGPipeHandle handle = tracker.Acquire(buffer);
|
||||
Uint16 bindMask = tracker.BindMask(handle);
|
||||
if (auto* ctx = LiveContext()) bindMask = tracker.RefreshBindMask(*ctx, buffer, handle);
|
||||
// storageDefined = false: the constructor has no store yet, storage is defined lazily
|
||||
// by the first respecify, and the backend's ensure path already tolerates a resource
|
||||
// that has none.
|
||||
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, false);
|
||||
tracker.NoteDesc(desc, true);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceRespecify(BufferObject& buffer) {
|
||||
MGPipeResourceTracker& tracker = MGPipeResourceTrackerInstance();
|
||||
const MGPipeHandle handle = tracker.Acquire(buffer);
|
||||
Uint16 bindMask = tracker.BindMask(handle);
|
||||
if (auto* ctx = LiveContext()) bindMask = tracker.RefreshBindMask(*ctx, buffer, handle);
|
||||
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true);
|
||||
tracker.NoteDesc(desc, false);
|
||||
// initialBytes is the client's own shadow base - zero copy, and null is a real answer
|
||||
// for the orphaning idiom (a NULL-data respecify leaves the store undefined and the
|
||||
// backend must not upload the stale bytes).
|
||||
const void* initialBytes = desc.HasDefinedContent != 0 ? buffer.MappedData() : nullptr;
|
||||
// kNeedsAck rides on the CALL and MGPipeResourceRespecifyNeedsAck(desc) decides per
|
||||
// record: only an immutable store (a glBufferStorage*) is a real synchronous
|
||||
// allocation and only it is allowed one. In monolith the acknowledgement is
|
||||
// ((void)0), because the applier is one function call away and has already run by
|
||||
// the time this returns; the transport wires the doorbell to that same predicate.
|
||||
MGPipeApplyResourceRespecify(desc, initialBytes);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceSubData(BufferObject& buffer, SizeT offset, SizeT size) {
|
||||
const MGPipeHandle handle = MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
const Uint8* base = buffer.MappedData();
|
||||
const Bool encodable =
|
||||
MGPipeForEachSubDataRecordRange(offset, size, [&](Uint64 at, Uint64 length) {
|
||||
MGPSubData record{};
|
||||
MGPipeBuildSubDataRecord(handle, at, length, record);
|
||||
MGPipeApplyResourceSubData(record, base + at);
|
||||
});
|
||||
if (!encodable) {
|
||||
MGLOG_E_ONCE("MGPipe: resource_subdata range [%llu, +%llu) on buffer %u cannot be encoded - "
|
||||
"one record's destination box caps the offset at 2^31-1",
|
||||
static_cast<unsigned long long>(offset), static_cast<unsigned long long>(size),
|
||||
buffer.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void MGPipeEmitBufferSubDataResident(BufferObject& buffer, SizeT offset, const void* bytes, SizeT size) {
|
||||
const MGPipeHandle handle = MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
const auto* base = static_cast<const Uint8*>(bytes);
|
||||
const Bool encodable =
|
||||
MGPipeForEachSubDataRecordRange(offset, size, [&](Uint64 at, Uint64 length) {
|
||||
MGPSubData record{};
|
||||
MGPipeBuildSubDataRecord(handle, at, length, record);
|
||||
// The application's STAGING store, valid for the duration of the call only.
|
||||
MGPipeApplyBufferSubDataResident(record, base + (at - offset));
|
||||
});
|
||||
if (!encodable) {
|
||||
MGLOG_E_ONCE("MGPipe: buffer_subdata_resident range [%llu, +%llu) on buffer %u cannot be encoded",
|
||||
static_cast<unsigned long long>(offset), static_cast<unsigned long long>(size),
|
||||
buffer.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceFlushRange(BufferObject& buffer, SizeT offset, SizeT size, Uint32 accessFlags) {
|
||||
const MGPipeHandle handle = MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
MGPFlushRange record{};
|
||||
record.Res = handle;
|
||||
record.Offset = offset;
|
||||
record.Size = size;
|
||||
// The application's REAL flags, not a normalised subset: the backend's kill-switch
|
||||
// arm reads INVALIDATE_RANGE / INVALIDATE_BUFFER / UNSYNCHRONIZED per call to choose
|
||||
// between a map+memcpy+unmap and an upload, so merging them here would change which.
|
||||
record.AccessFlags = accessFlags;
|
||||
MGPipeApplyResourceFlushRange(record, buffer.MappedData() + offset);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceReadback(BufferObject& buffer) {
|
||||
const MGPipeHandle handle = MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
MGPReadback record{};
|
||||
record.Res = handle;
|
||||
// Whole-buffer by contract (BufferObject.h: the op pulls the backend's current
|
||||
// contents for the WHOLE buffer into the shadow).
|
||||
record.Offset = 0;
|
||||
record.Size = buffer.GetSize();
|
||||
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's
|
||||
// epoch bump happens AFTER that writeback, never before.
|
||||
MGPipeApplyResourceReadback(record);
|
||||
}
|
||||
|
||||
void* MGPipeEmitMapPersistent(BufferObject& buffer) {
|
||||
const MGPipeHandle handle = MGPipeResourceTrackerInstance().Acquire(buffer);
|
||||
MGPipeResourceTrackerInstance().NoteMapPersistent();
|
||||
// THE map-persistent-roundtrips SITE, and it counts every EMISSION - mint OR
|
||||
// DECLINE - because every one of them needs an answer from the resource owner. A
|
||||
// counter defined as "round trips actually taken" is 0 by construction in monolith
|
||||
// and could never go red for the reason it exists; this one is the same number in
|
||||
// both modes and is "one per storage definition" exactly as the design requires.
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::MapPersistentRoundtrips, 1);
|
||||
}
|
||||
return MGPipeApplyMapPersistent(BufferHandleOnly(handle), buffer.GetSize(), buffer.MappedData());
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceDestroyAndFree(BufferObject& buffer) {
|
||||
MGPipeResourceTracker& tracker = MGPipeResourceTrackerInstance();
|
||||
const MGPipeHandle handle = tracker.Find(buffer);
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
if (MGPipeResourceSubsystemEnabled()) {
|
||||
tracker.NoteDestroy();
|
||||
MGPipeApplyResourceDestroy(BufferHandleOnly(handle));
|
||||
}
|
||||
// THE ORDER IS FIXED (D-L): the applier clears the record and the backend drops its
|
||||
// twin while the handle still resolves, and only then does the slot go back. Free
|
||||
// erases the lifetimeId -> slot mapping, so a notice resolved twice finds nothing the
|
||||
// second time - and the Gen bump happens on the NEXT handout of the slot, not here,
|
||||
// so a double free cannot skip a generation.
|
||||
tracker.Retire(handle);
|
||||
MGPipeSlots().Free(MGPipeKind::Buffer, handle);
|
||||
}
|
||||
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||
if (verb == nullptr || field == nullptr) {
|
||||
g_omission = PoisonOmission{};
|
||||
@@ -711,14 +877,28 @@ namespace MobileGL::MG_Pipe {
|
||||
// row to Coverage.def can never silently drop a field on the floor before the call
|
||||
// that carries it exists.
|
||||
//
|
||||
// P3a's two are DELIBERATELY ABSENT at the contract commit: the three emitters below
|
||||
// are stubs that emit nothing and the applier's fourteen entry points are stubs that
|
||||
// apply nothing, so wiring either bit here would retire a pull for a call that does
|
||||
// not happen yet. The commit that gives the emitters their bodies adds them.
|
||||
// P3a's two were DELIBERATELY ABSENT at the contract commit, because the emitters
|
||||
// were stubs; each is added by the commit that gives its own emitters their bodies.
|
||||
//
|
||||
// NEITHER OF THEM RETIRES A PULL, and saying so is the point of adding them
|
||||
// deliberately rather than by reflex:
|
||||
//
|
||||
// kMGPipeSubsystemResources names NO emitted field at all. SubsystemForEmitter
|
||||
// above can never return it, because the resource family is dispatched at the GL
|
||||
// call that causes it rather than filled into a PipeInputs field - there is no
|
||||
// Coverage.def emitted row for it and there cannot be one. It is here so the
|
||||
// constant states what this build emits for, which is what an operator reading
|
||||
// a MOBILEGL_PIPE_PUSH value has to be able to trust.
|
||||
//
|
||||
// kMGPipeSubsystemVertexInput names exactly one emitted field, GetBoundVertexArray
|
||||
// through bind_vertex_elements - and EmittedCallSuppliesTheWholeField below says
|
||||
// false for it, with the reason. So this bit switches the EMISSION on and
|
||||
// changes nothing about the fill loop.
|
||||
constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState |
|
||||
kMGPipeSubsystemPixelPack |
|
||||
kMGPipeSubsystemPatchState |
|
||||
kMGPipeSubsystemVertexAttribDefaults;
|
||||
kMGPipeSubsystemVertexAttribDefaults |
|
||||
kMGPipeSubsystemResources;
|
||||
|
||||
// A field an emitted call supplies COMPLETELY, so the residual fill may stop pulling
|
||||
// it. Two rows of Coverage.def's emitted list do not qualify and each has its reason
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/ResourceTracker.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
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P3a's resource family (brief D-A, D-B, D-C, D-D).
|
||||
//
|
||||
// WHERE IT RUNS, and it is the ONE exception to push-at-validate (ARCHITECTURE.md 5.1):
|
||||
// the seven BufferBackendOps hooks already dispatch at the GL call that causes them, so
|
||||
// their pipe calls are emitted from the same BufferObject dispatchers - not from
|
||||
// MGPipeValidateForVerb. Nothing about buffers moves to validate time in P3a.
|
||||
//
|
||||
// WHAT LIVES HERE
|
||||
// * the sticky BindMask, one constexpr BufferTarget -> bit table with a static_assert
|
||||
// that it covers every enumerator, so a new target cannot be silently unmapped;
|
||||
// * the lifetimeId -> {slot, gen} mint (through MGPipeSlots(), the one allocator) and
|
||||
// the slot -> BufferObject* INVERSE the reverse channel resolves a writeback through;
|
||||
// * the nine MGPipeEmitResource* bodies, declared in MG_Pipe/PipeMutation.h so that
|
||||
// MG_State sees a declaration and never this file (the same layering PipeMutation.h
|
||||
// already has for MGP_NOTE_MUTATION: declare in MG_Pipe, define in MG_Impl);
|
||||
// * the MGPSubData range splitter, because one record's box caps the destination at a
|
||||
// 2^31-1 offset and a 2^32-1 size;
|
||||
// * the map-persistent-roundtrips counting site.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h states in full: the root CMakeLists.txt
|
||||
// that would name a new .cpp belongs to the contract package and is frozen behind the tag.
|
||||
// MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
||||
//
|
||||
// NO TIMER, and no per-call record copy on a HOT path. The two observables a unit case
|
||||
// needs - the last emitted descriptor and the per-call counts - are written only by
|
||||
// resource_create and resource_respecify, which run once per glBufferData rather than per
|
||||
// upload; resource_subdata, the hot one, is observed through the pure builders below
|
||||
// instead (MGPipeBuildSubDataRecord / MGPipeForEachSubDataRecordRange), which is also what
|
||||
// lets a test drive the splitter at both of its bounds without a 4 GiB buffer.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <Config.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-A3: BindMask
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// MGPResourceDesc::BindMask's twelve bits, in the order MGPipeTypes.h names them:
|
||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY.
|
||||
//
|
||||
// They are spelled HERE rather than in MGPipeTypes.h because that header is the contract
|
||||
// package's and the mask has, so far, exactly one producer: this file. The integrator
|
||||
// moves them beside the field when a second producer appears (P4a's texture family).
|
||||
enum MGPipeBindBit : Uint16 {
|
||||
kMGPipeBindNone = 0,
|
||||
kMGPipeBindVertex = 1u << 0,
|
||||
kMGPipeBindIndex = 1u << 1,
|
||||
kMGPipeBindConstant = 1u << 2,
|
||||
kMGPipeBindShaderBuffer = 1u << 3,
|
||||
kMGPipeBindIndirect = 1u << 4,
|
||||
kMGPipeBindSampler = 1u << 5,
|
||||
kMGPipeBindShaderImage = 1u << 6,
|
||||
kMGPipeBindRenderTarget = 1u << 7,
|
||||
kMGPipeBindDepthStencil = 1u << 8,
|
||||
kMGPipeBindStreamOutput = 1u << 9,
|
||||
kMGPipeBindAtomic = 1u << 10,
|
||||
// THE D-B7 SWITCH. With kCapNeedsHostIndexBytes set the server mirrors this
|
||||
// resource's bytes so it can rewrite restart indices and flatten multi-draws
|
||||
// (ARCHITECTURE.md 10.3). Getting it wrong is invisible in monolith and silently
|
||||
// disables both under split, which is why it is set from the same table as every
|
||||
// other bit rather than from a special case at the emission site.
|
||||
kMGPipeBindElementArray = 1u << 11,
|
||||
};
|
||||
|
||||
// A sentinel the table below returns for an enumerator it does not name. It is NOT a
|
||||
// legal mask value: every enumerator must be listed, including the ones that map to no
|
||||
// bit at all, so that ADDING a BufferTarget is a build break here rather than a bit
|
||||
// that silently stops being published.
|
||||
inline constexpr Uint32 kMGPipeBindUnmapped = 0x10000u;
|
||||
|
||||
// The one table. No `default:` arm on purpose - that is what makes the static_assert
|
||||
// below able to see an unnamed enumerator.
|
||||
constexpr Uint32 MGPipeBindMaskForBufferTarget(BufferTarget target) {
|
||||
switch (target) {
|
||||
case BufferTarget::Vertex:
|
||||
return kMGPipeBindVertex;
|
||||
// GL_ELEMENT_ARRAY_BUFFER is the VAO's element slot: the same bind is both "this
|
||||
// resource is an index buffer" and "the server may need its bytes on its own side".
|
||||
case BufferTarget::Index:
|
||||
return kMGPipeBindIndex | kMGPipeBindElementArray;
|
||||
case BufferTarget::Uniform:
|
||||
return kMGPipeBindConstant;
|
||||
case BufferTarget::ShaderStorage:
|
||||
return kMGPipeBindShaderBuffer;
|
||||
case BufferTarget::DispatchIndirect:
|
||||
case BufferTarget::DrawIndirect:
|
||||
case BufferTarget::Parameter:
|
||||
return kMGPipeBindIndirect;
|
||||
// A texture buffer's backing store is SAMPLED through the texture that names it.
|
||||
case BufferTarget::Texture:
|
||||
return kMGPipeBindSampler;
|
||||
case BufferTarget::TransformFeedback:
|
||||
return kMGPipeBindStreamOutput;
|
||||
case BufferTarget::AtomicCounter:
|
||||
return kMGPipeBindAtomic;
|
||||
// TRANSFER AND QUERY TARGETS, which the bind mask deliberately does not name: none
|
||||
// of them is a pipeline binding, none of them makes the server keep anything, and
|
||||
// a bit set for them would only widen what a split server mirrors. Listed rather
|
||||
// than defaulted, so the completeness assert still sees them.
|
||||
case BufferTarget::CopyRead:
|
||||
case BufferTarget::CopyWrite:
|
||||
case BufferTarget::PixelPack:
|
||||
case BufferTarget::PixelUnpack:
|
||||
case BufferTarget::Query:
|
||||
return kMGPipeBindNone;
|
||||
case BufferTarget::BufferTargetCount:
|
||||
case BufferTarget::Unknown:
|
||||
return kMGPipeBindNone;
|
||||
}
|
||||
return kMGPipeBindUnmapped;
|
||||
}
|
||||
|
||||
constexpr Bool MGPipeEveryBufferTargetIsMapped() {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(BufferTarget::BufferTargetCount); ++i) {
|
||||
if (MGPipeBindMaskForBufferTarget(static_cast<BufferTarget>(i)) == kMGPipeBindUnmapped) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(MGPipeEveryBufferTargetIsMapped(),
|
||||
"a BufferTarget enumerator has no MGPResourceDesc::BindMask row: add it to "
|
||||
"MGPipeBindMaskForBufferTarget, including a deliberate kMGPipeBindNone, or the "
|
||||
"resource it is bound to stops publishing that binding (D-A3, P8 expectation 1)");
|
||||
static_assert(MGPipeBindMaskForBufferTarget(BufferTarget::Index) & kMGPipeBindElementArray,
|
||||
"the ELEMENT_ARRAY bit is the index host mirror's switch (ARCHITECTURE.md 10.3)");
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The discriminators MGPResourceDesc / MGPSubData carry for a BUFFER
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MGPipeTypes.h documents Target as "Buffer | Tex1D..TexCubeArray | Renderbuffer |
|
||||
// TexBuffer" and StorageKind as "== TextureStorageType", but P3a is buffer-only and the
|
||||
// contract package minted no enum for the first list. Buffer is its leading member and
|
||||
// is therefore 0, which is also what a zero-initialised record already says; the second
|
||||
// is the frontend enum, named rather than open-coded.
|
||||
inline constexpr Uint16 kMGPipeResourceTargetBuffer = 0;
|
||||
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
||||
static_cast<Uint8>(MobileGL::TextureStorageType::Buffer);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-A2: the payload builders. Pure, so a unit case can assert field by field.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The descriptor for `buffer`. `storageDefined` is false for the create that the
|
||||
// constructor emits - storage is defined lazily by the first respecify and a backend
|
||||
// tolerates a resource that has none - and true for every respecify.
|
||||
inline MGPResourceDesc MGPipeBuildResourceDesc(const MG_State::GLState::BufferObject& buffer,
|
||||
MGPipeHandle handle, Uint16 bindMask,
|
||||
Bool storageDefined) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = handle;
|
||||
desc.Target = static_cast<Uint8>(kMGPipeResourceTargetBuffer);
|
||||
desc.StorageKind = kMGPipeResourceStorageKindBuffer;
|
||||
desc.BindMask = bindMask;
|
||||
if (storageDefined) {
|
||||
desc.Width = static_cast<Uint32>(buffer.GetSize());
|
||||
desc.Usage = static_cast<Uint32>(buffer.GetUsage());
|
||||
desc.StorageFlags = static_cast<Uint32>(buffer.GetStorageFlags());
|
||||
desc.Immutable = buffer.IsImmutableStorage() ? 1 : 0;
|
||||
desc.HasDefinedContent = buffer.HasDefinedContent() ? 1 : 0;
|
||||
}
|
||||
// Diagnostics only: a GL name is never an identity, never a memo key and never part
|
||||
// of a content hash (ARCHITECTURE.md 4.2.1).
|
||||
desc.GlNameForDiag = static_cast<Uint32>(buffer.GetExternalIndex());
|
||||
return desc;
|
||||
}
|
||||
|
||||
// The buffer half of MGPSubData: the destination range rides in the box's first
|
||||
// coordinate and first extent, and MGPipeSetSubDataBufferRange is the ONLY spelling of
|
||||
// that convention. Returns false, with the record untouched, when the range does not fit
|
||||
// one record - which is where MGPipeForEachSubDataRecordRange comes in.
|
||||
inline Bool MGPipeBuildSubDataRecord(MGPipeHandle res, Uint64 offset, Uint64 size, MGPSubData& out) {
|
||||
out = MGPSubData{};
|
||||
out.Res = res;
|
||||
out.Target = kMGPipeResourceTargetBuffer;
|
||||
out.SourceIsVerbatimLevelShadow = 1; // the bytes ARE the client's shadow, unmodified
|
||||
if (!MGPipeSetSubDataBufferRange(out, offset, size)) return false;
|
||||
out.Blob.Seg = kMGHostSpanSegNone;
|
||||
out.Blob.Size = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ONE record caps at a 2^31-1 offset and a 2^32-1 size (MGPipeTypes.h), so a range
|
||||
// beyond either has to be split. The pieces are CONTIGUOUS and in ascending order:
|
||||
// splitting a content write into overlapping or reordered pieces would change what the
|
||||
// backend's queue-and-drain sees, and the Mali WAR-stall fix depends on the queue being
|
||||
// exactly the writes the application made.
|
||||
//
|
||||
// The OFFSET bound cannot be split away - every piece of a range that starts past
|
||||
// 2^31-1 starts past it too - so the walk returns false for such a range and emits
|
||||
// nothing rather than emitting a record whose box the applier's bounds gate would
|
||||
// refuse. That needs a >2 GiB buffer, which nothing in the corpus has; the answer is
|
||||
// still stated rather than assumed, because the alternative is a silent truncation.
|
||||
template <class Fn>
|
||||
inline Bool MGPipeForEachSubDataRecordRange(Uint64 offset, Uint64 size, Fn&& piece) {
|
||||
constexpr Uint64 kMaxOffset = 0x7FFFFFFFull;
|
||||
constexpr Uint64 kMaxSize = 0xFFFFFFFFull;
|
||||
if (offset > kMaxOffset) return false;
|
||||
if (size == 0) return true;
|
||||
// A single piece may run to the end of the buffer; only its SIZE is split.
|
||||
Uint64 at = offset;
|
||||
Uint64 left = size;
|
||||
while (left > 0) {
|
||||
if (at > kMaxOffset) return false;
|
||||
const Uint64 chunk = left > kMaxSize ? kMaxSize : left;
|
||||
piece(at, chunk);
|
||||
at += chunk;
|
||||
left -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The tracker: handles, the inverse, the sticky mask, the reverse channel
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
class MGPipeResourceTracker {
|
||||
public:
|
||||
using BufferObject = MG_State::GLState::BufferObject;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// The handle for `buffer`, minted on first use. Minting is NOT gated on a backend
|
||||
// having registered MGPipeResourceOps: the handle is CLIENT state and
|
||||
// set_vertex_buffers names it whether or not the resource family is switched on, so
|
||||
// gating it would make the vertex-input subsystem emit null handles whenever the
|
||||
// resource subsystem is off. Only the CALLS are gated (D-A1).
|
||||
MGPipeHandle Acquire(BufferObject& buffer) {
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
||||
m_bySlot[slot].Object = &buffer;
|
||||
m_bySlot[slot].Gen = handle.Gen;
|
||||
return handle;
|
||||
}
|
||||
|
||||
// The handle a buffer already has, or the null handle. Never mints - the emission
|
||||
// path calls Acquire, the query paths call this.
|
||||
MGPipeHandle Find(const BufferObject& buffer) const {
|
||||
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||
}
|
||||
|
||||
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
|
||||
// create the constructor emits and the destroy the destructor emits, and a readback
|
||||
// is only ever issued for a live, bound buffer. A WeakPtr would be wrong - the
|
||||
// object does not own itself through a SharedPtr at those two moments. The Gen
|
||||
// compare is what refuses a stale handle rather than resolving it to whatever now
|
||||
// occupies the slot.
|
||||
BufferObject* Resolve(MGPipeHandle handle) const {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (MGPipeHandleIsNull(handle) || slot >= m_bySlot.size()) return nullptr;
|
||||
const Entry& entry = m_bySlot[slot];
|
||||
if (entry.Object == nullptr || entry.Gen != handle.Gen) return nullptr;
|
||||
if (MGPipeSlots().GenOfSlot(MGPipeKind::Buffer, handle.Slot) != handle.Gen) return nullptr;
|
||||
return entry.Object;
|
||||
}
|
||||
|
||||
// Drops the inverse entry and the sticky mask. The CALLER frees the slot afterwards,
|
||||
// in that order (D-L): MGPipeSlotAllocator::Free erases the lifetimeId -> slot
|
||||
// mapping, so anything that has to resolve the handle must do it first.
|
||||
void Retire(MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_bySlot.size()) return;
|
||||
m_bySlot[slot] = Entry{};
|
||||
}
|
||||
|
||||
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
|
||||
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
|
||||
// keeps saying so.
|
||||
Uint16 BindMask(MGPipeHandle handle) const {
|
||||
const SizeT slot = handle.Slot;
|
||||
return slot < m_bySlot.size() ? m_bySlot[slot].BindMask : Uint16{0};
|
||||
}
|
||||
|
||||
void NoteBoundAs(MGPipeHandle handle, BufferTarget target) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_bySlot.size()) return;
|
||||
m_bySlot[slot].BindMask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
|
||||
}
|
||||
|
||||
// Accumulates into the sticky mask every target `buffer` is bound to RIGHT NOW, and
|
||||
// returns the accumulated value.
|
||||
//
|
||||
// [DEVIATION, recorded in client-v1.md] D-A3 asks for the OR at every glBindBuffer /
|
||||
// glBindBufferBase / glBindBufferRange / VAO element-slot bind. Those entry points
|
||||
// are MG_Impl/GLImpl/Buffer/GL_Buffer.cpp's, which C.5 assigns to no package and
|
||||
// C.1 does not list for this one, so the mask is accumulated by SAMPLING the
|
||||
// frontend's live binding state instead - here, at every resource emission, which is
|
||||
// the only place its value is read. It is still STICKY (the union over every sample
|
||||
// this buffer has ever been part of), and it is exact for the GL idiom the bit
|
||||
// matters for: bind, then define or update the store. What it cannot see is a bind
|
||||
// that happens after the buffer's LAST storage or content operation and is never
|
||||
// followed by another - the fix is one line in BindBuffer_State, and it is handed to
|
||||
// the integrator rather than taken here.
|
||||
//
|
||||
// The scan is skipped unless a binding-slot version moved since the last one, which
|
||||
// is one Uint16 load per global target and none per binding point.
|
||||
Uint16 RefreshBindMask(GLContext& ctx, const BufferObject& buffer, MGPipeHandle handle) {
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_bySlot.size()) return 0;
|
||||
Entry& entry = m_bySlot[slot];
|
||||
const Uint64 epoch = BindEpoch(ctx);
|
||||
if (epoch == m_bindEpoch && entry.BindMaskEpoch == epoch) return entry.BindMask;
|
||||
m_bindEpoch = epoch;
|
||||
entry.BindMaskEpoch = epoch;
|
||||
Uint16 mask = entry.BindMask;
|
||||
for (const auto target : MG_State::GLState::GlobalBufferTargets) {
|
||||
if (ctx.GetBufferBindingSlot(target).GetBoundObject().get() == &buffer) {
|
||||
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
|
||||
}
|
||||
}
|
||||
for (const auto target : MG_State::GLState::BufferBindPointTargets) {
|
||||
const SizeT touched = ctx.GetTouchedBufferBindingPointCount(target);
|
||||
for (SizeT i = 0; i < touched; ++i) {
|
||||
if (ctx.GetBufferBindingPoint(target, static_cast<Uint>(i)).GetBoundObject().get() == &buffer) {
|
||||
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The index slot is the BOUND VAO's, not BufferState's, so it is not in
|
||||
// GlobalBufferTargets and GetBufferBindingSlot(Index) asserts without a VAO.
|
||||
if (const auto& vao = ctx.GetBoundVertexArray()) {
|
||||
if (vao->GetIndexBufferBindingSlot().GetBoundObject().get() == &buffer) {
|
||||
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(BufferTarget::Index));
|
||||
}
|
||||
for (int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||
if (vao->GetAttribute(static_cast<Uint>(i)).Buffer.get() == &buffer) {
|
||||
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(BufferTarget::Vertex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.BindMask = mask;
|
||||
return mask;
|
||||
}
|
||||
|
||||
// ---- the two observables a unit case reads (see the header comment) ----
|
||||
const MGPResourceDesc& LastDesc() const { return m_lastDesc; }
|
||||
Uint64 CreateCount() const { return m_creates; }
|
||||
Uint64 RespecifyCount() const { return m_respecifies; }
|
||||
Uint64 DestroyCount() const { return m_destroys; }
|
||||
Uint64 MapPersistentCount() const { return m_mapPersistents; }
|
||||
|
||||
void NoteDesc(const MGPResourceDesc& desc, Bool isCreate) {
|
||||
m_lastDesc = desc;
|
||||
if (isCreate) {
|
||||
++m_creates;
|
||||
} else {
|
||||
++m_respecifies;
|
||||
}
|
||||
}
|
||||
void NoteDestroy() { ++m_destroys; }
|
||||
void NoteMapPersistent() { ++m_mapPersistents; }
|
||||
|
||||
// A unit fixture's per-case reset. Never called by the library: a context change
|
||||
// does not invalidate a handle, because the handle is the CLIENT's identity for a
|
||||
// frontend object that outlives it.
|
||||
void ResetForTest() {
|
||||
m_bySlot.clear();
|
||||
m_bindEpoch = 0;
|
||||
m_lastDesc = MGPResourceDesc{};
|
||||
m_creates = m_respecifies = m_destroys = m_mapPersistents = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
BufferObject* Object = nullptr;
|
||||
Uint32 Gen = 0;
|
||||
Uint16 BindMask = 0;
|
||||
Uint64 BindMaskEpoch = 0;
|
||||
};
|
||||
|
||||
// "Has any buffer binding moved since the last scan": the sum of the binding-slot
|
||||
// versions, which BindingSlot bumps only on a real change. A collision costs one
|
||||
// skipped rescan of ONE buffer's mask, and the mask is re-scanned at the next
|
||||
// emission whose epoch differs, so it can delay a bit by one storage op and never
|
||||
// drop one - the same over-fire-is-free / under-fire-is-fatal direction every
|
||||
// shutter in Tracker.h takes.
|
||||
static Uint64 BindEpoch(GLContext& ctx) {
|
||||
Uint64 epoch = 1;
|
||||
for (const auto target : MG_State::GLState::GlobalBufferTargets) {
|
||||
epoch += ctx.GetBufferBindingSlot(target).GetVersion();
|
||||
epoch *= 3;
|
||||
}
|
||||
if (const auto& vao = ctx.GetBoundVertexArray()) {
|
||||
epoch += vao->GetIndexBufferBindingSlot().GetVersion();
|
||||
epoch = MGPipeMixShutterValue(epoch, vao->GetLifetimeId());
|
||||
epoch = MGPipeMixShutterValue(epoch, vao->GetConfigVersion());
|
||||
}
|
||||
return epoch;
|
||||
}
|
||||
|
||||
// The same mix Tracker.h's composite shutters use. Spelled here rather than
|
||||
// included so this header does not depend on the tracker.
|
||||
static constexpr Uint64 MGPipeMixShutterValue(Uint64 accumulator, Uint64 value) {
|
||||
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
Vector<Entry> m_bySlot;
|
||||
Uint64 m_bindEpoch = 0;
|
||||
MGPResourceDesc m_lastDesc{};
|
||||
Uint64 m_creates = 0;
|
||||
Uint64 m_respecifies = 0;
|
||||
Uint64 m_destroys = 0;
|
||||
Uint64 m_mapPersistents = 0;
|
||||
};
|
||||
|
||||
// The monolith's one resource tracker, beside the state tracker, the CSO cache and the
|
||||
// set-hash suppressor.
|
||||
inline MGPipeResourceTracker& MGPipeResourceTrackerInstance() {
|
||||
static MGPipeResourceTracker tracker;
|
||||
return tracker;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-D: the client's half of the reverse channel
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The backend produced the bytes of a readback and hands them back through the channel.
|
||||
// The client resolves the handle to its own object and writes the shadow; the epoch bump
|
||||
// stays SERVER-side and happens AFTER this returns, never before (ARCHITECTURE.md 7.4:
|
||||
// the reverse channel needs the same ordering guarantee as the forward one).
|
||||
inline void MGPipeClientOnBufferWriteback(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes) {
|
||||
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
|
||||
if (buffer == nullptr) {
|
||||
MGLOG_E_ONCE("MGPipe: OnBufferWriteback for a handle {%u,%u} that resolves to no buffer",
|
||||
res.Slot, res.Gen);
|
||||
return;
|
||||
}
|
||||
if (bytes.Seg != kMGHostSpanSegNone) {
|
||||
MGLOG_E_ONCE("MGPipe: OnBufferWriteback carried a transport segment (%u); P3a is monolith only",
|
||||
bytes.Seg);
|
||||
return;
|
||||
}
|
||||
// Monolith: Seg is kMGHostSpanSegNone and Offset IS the address of the backend's
|
||||
// mapped bytes (MGPipeTypes.h says so in as many words). Under a transport the
|
||||
// segment resolves first, and that is the phase's edit, not this one's.
|
||||
buffer->WritebackFromBackend(
|
||||
DataPtr{reinterpret_cast<void*>(static_cast<std::uintptr_t>(bytes.Offset)),
|
||||
static_cast<SizeT>(bytes.Size)},
|
||||
static_cast<SizeT>(offset));
|
||||
}
|
||||
|
||||
// A draw or dispatch wrote these ranges. ARCHITECTURE.md 7.1 calls this a NARROWING
|
||||
// channel - the client builds a conservative pending set at its own emission points and
|
||||
// the callback only ever removes from it - so P3a's implementation marks exactly what
|
||||
// the three Espryt MarkGpuWritten sites mark today and the observable behaviour is
|
||||
// unchanged. The narrowing itself is P8/P9's.
|
||||
inline void MGPipeClientOnGpuWritten(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges) {
|
||||
(void)rangeCount;
|
||||
(void)ranges;
|
||||
if (auto* buffer = MGPipeResourceTrackerInstance().Resolve(res)) buffer->MarkGpuWritten();
|
||||
}
|
||||
|
||||
// Installed once, and never over an entry a backend already claimed: these two are the
|
||||
// CLIENT's implementations of a backend -> frontend callback, so the backend installs
|
||||
// the rest of the table and these two answer for it.
|
||||
inline void MGPipeInstallClientResourceCallbacks() {
|
||||
if (gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
||||
gMGPipeCallbacks.OnBufferWriteback = &MGPipeClientOnBufferWriteback;
|
||||
}
|
||||
if (gMGPipeCallbacks.OnGpuWritten == nullptr) {
|
||||
gMGPipeCallbacks.OnGpuWritten = &MGPipeClientOnGpuWritten;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
Reference in New Issue
Block a user