mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +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
|
||||
@@ -75,6 +75,59 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// MG_Impl/Pipe/PipeFill.cpp. A no-op unless a context is live.
|
||||
void MGPipeNoteAggregate(MGPipeAggregate aggregate);
|
||||
|
||||
// ---- P3a: the resource family's emission points (brief D-A1) ----
|
||||
//
|
||||
// The seven BufferBackendOps hooks already dispatch at the GL call that causes them
|
||||
// (ARCHITECTURE.md 5.1 names them as the ONE exception to push-at-validate), so their
|
||||
// pipe calls are emitted from the same BufferObject dispatchers rather than from the
|
||||
// validate point. That puts the emission inside MG_State, which is why these are
|
||||
// DECLARED here beside the two notices and DEFINED in MG_Impl/Pipe/PipeFill.cpp: this
|
||||
// header is the one MG_State already includes for exactly this, and the closure gate
|
||||
// (check_include_closure.py's mutation-header probe) keeps it a declaration - reaching
|
||||
// MG_Impl/Pipe/ResourceTracker.h from BufferObject.cpp would pull the client's tracker
|
||||
// into the state machine that calls it.
|
||||
//
|
||||
// The forward declaration is the whole coupling: none of these needs the definition of
|
||||
// BufferObject, and this header must not gain it.
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class BufferObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// (Features.PipePush & kMGPipeSubsystemResources) != 0 && MGPipeGetResourceOps() != nullptr.
|
||||
//
|
||||
// BOTH HALVES MATTER. The bit is the operator's per-subsystem A/B; the table is "has a
|
||||
// backend taken this family over at all". Until one has, every dispatch below falls
|
||||
// through to the BufferBackendOps table it replaces and the tree behaves exactly as it
|
||||
// did - which is what lets the client half land on its own.
|
||||
Bool MGPipeResourceSubsystemEnabled();
|
||||
// The nullable member, asked the way the frontend asks g_bufferBackendOps->ResidentSubData
|
||||
// today: one backend deliberately does not implement it and the caller has a different
|
||||
// path when it is absent (BufferObject::FillSubData).
|
||||
Bool MGPipeResourceOpsHaveSubDataResident();
|
||||
|
||||
// Minted from the constructor and released from the destructor, both unconditionally in
|
||||
// a push build: a handle is CLIENT state and set_vertex_buffers names it whether or not
|
||||
// the resource family is switched on. The CALLS are what the predicate above gates.
|
||||
void MGPipeMintResourceHandle(MG_State::GLState::BufferObject& buffer);
|
||||
// In this order, and it is not negotiable (D-L): the destroy resolves the handle, and
|
||||
// MGPipeSlotAllocator::Free erases the lifetimeId -> slot mapping it resolves through.
|
||||
void MGPipeEmitResourceDestroyAndFree(MG_State::GLState::BufferObject& buffer);
|
||||
|
||||
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
||||
void MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
||||
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
||||
void MGPipeEmitBufferSubDataResident(MG_State::GLState::BufferObject& buffer, SizeT offset,
|
||||
const void* bytes, SizeT size);
|
||||
void MGPipeEmitResourceFlushRange(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size,
|
||||
Uint32 accessFlags);
|
||||
void MGPipeEmitResourceReadback(MG_State::GLState::BufferObject& buffer);
|
||||
// Returns the coherent host pointer the resource owner donated, or null for a DECLINE -
|
||||
// which is a real answer. Every call, mint or decline, is one map-persistent roundtrip.
|
||||
void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer);
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_NOTE_MUTATION(Field) \
|
||||
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
||||
|
||||
@@ -34,9 +34,28 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
BufferObject::BufferObject(Uint externalIndex)
|
||||
: m_externalIndex(externalIndex), m_size(0), m_usage(BufferUsage::StaticDraw), m_isMapped(false),
|
||||
m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} {}
|
||||
m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a D-A2: a resource EXISTS before anything can name it, so resource_create is
|
||||
// emitted from the constructor and carries no storage - the store is defined lazily
|
||||
// by the first respecify and every backend already tolerates a resource with none.
|
||||
// The handle itself is minted whatever the subsystem bitmask says, because
|
||||
// set_vertex_buffers names this buffer by handle out of a different subsystem.
|
||||
MG_Pipe::MGPipeMintResourceHandle(*this);
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) MG_Pipe::MGPipeEmitResourceCreate(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
BufferObject::~BufferObject() {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P3a D-L: the buffer's death crosses as resource_destroy, which is the catalogue
|
||||
// call for it - no seventh NotifyStateObjectDestroyed raiser is added, because that
|
||||
// header exists for kinds that have no such call. The emit-then-free ORDER is fixed
|
||||
// inside the helper and is not negotiable.
|
||||
const Bool pushedResources = MG_Pipe::MGPipeResourceSubsystemEnabled();
|
||||
MG_Pipe::MGPipeEmitResourceDestroyAndFree(*this);
|
||||
if (pushedResources) return;
|
||||
#endif
|
||||
if (m_resource.Backend() && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) {
|
||||
g_bufferBackendOps->OnDestroy(m_resource.ReleaseBackend());
|
||||
}
|
||||
@@ -45,6 +64,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
void BufferObject::NotifyRespecify() {
|
||||
++m_changeSerial;
|
||||
MGP_NOTE_AGGREGATE(BufferChange);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
MG_Pipe::MGPipeEmitResourceRespecify(*this);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->Respecify) {
|
||||
g_bufferBackendOps->Respecify(*this);
|
||||
}
|
||||
@@ -55,6 +80,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGP_NOTE_AGGREGATE(BufferChange);
|
||||
if (size == 0) return;
|
||||
m_hasDefinedContent = true;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
MG_Pipe::MGPipeEmitResourceSubData(*this, offset, size);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
||||
g_bufferBackendOps->SubData(*this, offset, size);
|
||||
}
|
||||
@@ -65,6 +96,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGP_NOTE_AGGREGATE(BufferChange);
|
||||
if (range.start >= range.end) return;
|
||||
m_hasDefinedContent = true;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
static_assert(sizeof(appAccess.GetRaw()) <= sizeof(Uint32),
|
||||
"MGPFlushRange::AccessFlags is a Uint32 and carries the application's "
|
||||
"real Flags<BufferMappingAccessBit>, unnormalised");
|
||||
MG_Pipe::MGPipeEmitResourceFlushRange(*this, range.start, range.end - range.start,
|
||||
static_cast<Uint32>(appAccess.GetRaw()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
||||
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
||||
}
|
||||
@@ -189,6 +230,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (m_size < kLargeBufferAdoptBytes) return;
|
||||
if (m_resource.IsGpuResident()) return;
|
||||
if (m_isMapped) return;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
if (void* base = MG_Pipe::MGPipeEmitMapPersistent(*this)) m_resource.AdoptPersistentMap(base);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
||||
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
@@ -321,6 +368,17 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Cleared unconditionally: without a readback op the shadow can never catch up,
|
||||
// and retrying on every subsequent read would only repeat the same no-op.
|
||||
m_gpuWritePending = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
// The answer comes back through the reverse channel's OnBufferWriteback, which
|
||||
// resolves this handle to this object and writes the shadow before the server
|
||||
// bumps its mutation epoch. In monolith the whole sequence is synchronous inside
|
||||
// the applier, so the caller sees the reconciled shadow on return exactly as it
|
||||
// does today.
|
||||
MG_Pipe::MGPipeEmitResourceReadback(*this);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -369,6 +427,30 @@ namespace MobileGL::MG_State::GLState {
|
||||
// NotifyContentWrite on a resident store only bumps the serial: the backend has no
|
||||
// separate copy to sync, so no transfer op runs.
|
||||
void BufferObject::LandBytesIntoResidentStore(SizeT offset, DataPtr bytes) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// buffer_subdata_resident stays NULLABLE and stays asymmetric: one backend
|
||||
// deliberately does not implement it, and the frontend checks the pipe table exactly
|
||||
// as it checks the op table it replaces, so a backend without it keeps the legacy
|
||||
// ordered in-place host write below.
|
||||
if (bytes.size > 0 && MG_Pipe::MGPipeResourceSubsystemEnabled() &&
|
||||
MG_Pipe::MGPipeResourceOpsHaveSubDataResident()) {
|
||||
MG_Pipe::MGPipeEmitBufferSubDataResident(*this, offset, bytes.data, bytes.size);
|
||||
m_hasDefinedContent = true;
|
||||
++m_changeSerial;
|
||||
MGP_NOTE_AGGREGATE(BufferChange);
|
||||
m_gpuWritePending = true;
|
||||
return;
|
||||
}
|
||||
// No resident op: the write lands in place below, after retiring the GPU writes this
|
||||
// store is known to be waiting on - which is the same answer, and the same code, a
|
||||
// backend with a null ResidentSubData gets today.
|
||||
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
SyncGpuWrites();
|
||||
Memcpy(m_resource.Bytes() + offset, bytes.data, bytes.size);
|
||||
NotifyContentWrite(offset, bytes.size);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (bytes.size > 0 && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||
g_bufferBackendOps->ResidentSubData(*this, offset, bytes);
|
||||
m_hasDefinedContent = true;
|
||||
@@ -402,7 +484,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
// op the landing would memcpy the expansion into the mapping the loop below
|
||||
// fills in place anyway, so a whole-arena clear would allocate a whole arena
|
||||
// for nothing.
|
||||
if (m_resource.IsGpuResident() && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||
if (m_resource.IsGpuResident() &&
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The same question, asked of whichever table owns the family in this build.
|
||||
(MG_Pipe::MGPipeResourceSubsystemEnabled()
|
||||
? MG_Pipe::MGPipeResourceOpsHaveSubDataResident()
|
||||
: (g_bufferBackendOps && g_bufferBackendOps->ResidentSubData))
|
||||
#else
|
||||
g_bufferBackendOps && g_bufferBackendOps->ResidentSubData
|
||||
#endif
|
||||
) {
|
||||
Vector<Uint8> expanded(size);
|
||||
if (pattern.size == 1) {
|
||||
Memset(expanded.data(), *static_cast<const Uint8*>(pattern.data), size);
|
||||
@@ -504,6 +595,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (m_isMapped) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this);
|
||||
if (pushedBase == nullptr) return false;
|
||||
m_resource.AdoptPersistentMap(pushedBase);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -548,6 +647,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
// returning; AdoptPersistentMap then releases the shadow. Falls back to the
|
||||
// shadow when the backend declines (returns null). Only attempted once - the
|
||||
// storage is immutable and outlives unmap/remap.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
|
||||
!(access & BufferMappingAccessBit::FlushExplicit) &&
|
||||
MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||
if (void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this)) {
|
||||
m_resource.AdoptPersistentMap(pushedBase);
|
||||
}
|
||||
return m_resource.Bytes() + range.start;
|
||||
}
|
||||
#endif
|
||||
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
|
||||
!(access & BufferMappingAccessBit::FlushExplicit) && g_bufferBackendOps &&
|
||||
g_bufferBackendOps->AcquirePersistentMap) {
|
||||
|
||||
Reference in New Issue
Block a user