Files
MobileGL/MobileGL/MG_State/GLState/BufferState/BufferObject.h
T

313 lines
18 KiB
C++

// MobileGL - MobileGL/MG_State/GLState/BufferState/BufferObject.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>
#include <MG_Util/Math/VectorTypes.h>
#include "PipeResource.h"
namespace MobileGL {
enum class BufferTarget {
Vertex,
Index,
Uniform,
CopyRead,
CopyWrite,
PixelPack,
PixelUnpack,
Query,
Texture,
TransformFeedback,
AtomicCounter,
DispatchIndirect,
DrawIndirect,
Parameter,
ShaderStorage,
BufferTargetCount,
Unknown = -1
};
enum class BufferUsage {
StreamDraw,
StreamRead,
StreamCopy,
StaticDraw,
StaticRead,
StaticCopy,
DynamicDraw,
DynamicRead,
DynamicCopy,
Unknown = -1
};
enum class BufferMappingAccessBit : Uint {
Null = 0x00,
Read = 0x01,
Write = 0x02,
InvalidateRange = 0x04,
InvalidateBuffer = 0x08,
FlushExplicit = 0x10,
Unsynchronized = 0x20,
Persistent = 0x40,
Coherent = 0x80
};
namespace MG_State::GLState {
class BufferObject;
// BackendBufferResource and PipeResource (the storage abstraction that holds
// either the CPU shadow or the backend's persistently-mapped GPU memory) live
// in PipeResource.h.
class BufferObject {
public:
using TargetEnum = BufferTarget;
BufferObject(Uint externalIndex);
~BufferObject();
BufferObject(const BufferObject&) = delete;
BufferObject& operator=(const BufferObject&) = delete;
// Storage definition (single backend Respecify): glBufferData.
void Respecify(SizeT size, const void* data);
// Storage definition without contents; equivalent to Respecify(size, nullptr).
void Resize(SizeT size);
void AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags);
void SetUsage(BufferUsage usage);
void UploadData(DataPtr data, SizeT atOffset);
void UploadSubData(DataPtr data, SizeT atOffset);
// Repeats one already-converted element through [atOffset, atOffset + size) and
// publishes the range as one content mutation.
void FillSubData(DataPtr pattern, SizeT atOffset, SizeT size);
// Reads `size` bytes from the CPU shadow at `atOffset` into `dst` (glGetBufferSubData).
// The shadow reflects CPU writes (BufferData/SubData/maps) and backend write-backs, but not
// arbitrary GPU-side writes.
void DownloadSubData(void* dst, SizeT atOffset, SizeT size) const;
void CopyDataFrom(const SharedPtr<BufferObject>& src, SizeT srcOffset, SizeT dstOffset, SizeT size);
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
// Adopt backend host-visible coherent GPU storage as the source of truth
// (used for GPU-written targets like transform feedback capture, so
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
// resident, while the buffer is mapped (adoption releases the shadow a
// mapping may have handed the application), or when the backend declines.
Bool EnsureGpuResidentStorage();
// Unmap. A write map's staged bytes land in the store on the way out, unless
// the caller is about to replace that store (a respecification) and passes
// false - landing them there would copy a whole mapped range into storage
// being handed back on the next line.
void ReleaseMemory(Bool landStagedWrites = true);
void FlushMemoryRange(SizeT offset, SizeT length);
// Pushes the persistently-mapped write range to the backend; called by
// backends at draw time (persistent maps mutate the shadow without API calls).
void SyncPersistentMappedRange();
// Shadow-only write used when the backend copies GPU results (e.g. ReadPixels
// into a pixel-pack buffer) back into the frontend mirror. Does not issue a
// backend op: the backend storage already holds these bytes.
//
// `producedAtSeq` is the command seq the bytes are current as of (the event's
// ProducedAtSeq); 0 means the writeback is synchronous with its request and needs
// no ordering. A whole-buffer writeback clears the outstanding GPU write only when
// it is newer than the record the latest mark was taken for - an older one (a
// capture's writeback drained after a later dispatch re-marked the buffer) still
// lands its bytes but cannot vouch for the write it predates.
void WritebackFromBackend(DataPtr data, SizeT atOffset, Uint64 producedAtSeq = 0);
// A draw or dispatch just ran with this buffer bound where a shader can write
// it (shader storage / atomic counter). The next read has to reconcile with
// that: pull the bytes back, or - when the shadow already IS coherent GPU
// memory - wait for the work that wrote them to retire. Which of the two is
// the backend's business; the flag only says a GPU write is outstanding.
void MarkGpuWritten();
// The same mark, stamped with an explicit command seq: the mark stands for GPU
// work in records AFTER `orderSeq`. MarkGpuWritten stamps the session's last
// published seq (0 without a session).
void MarkGpuWrittenAfter(Uint64 orderSeq);
// The mark a server announces on the reverse channel (OnGpuWritten). That channel
// is in order with the writebacks, so every writeback drained after this mark was
// produced after the write it announces: the standing stamp is kept, not raised to
// the drain point - raising it would outrank the very readback the next read is
// already waiting on.
void MarkGpuWrittenInChannelOrder();
// Refreshes the shadow from the backend when a GPU write is outstanding. Called
// from every path that reads the shadow on the app's behalf.
void SyncGpuWrites();
// ---- P5 b1: the two things a split build has to do that a monolith does not ---
//
// Everything here is behind the build option AND behind
// `MG_Config::Transport != Monolith` at the call site, because an extra
// resource_subdata record or an extra respecify on the monolith path is new
// behaviour and D-J forbids it. The pull build compiles none of it, which is also
// how G1 holds over a file this central.
// ONE BLOCK of the mapped span (PersistentMapTracker::BlockBytes), emitted through
// the private NotifySubData - the same serial, the same defined-content flag, the
// same record - so the split arm and the monolith arm differ in HOW the span is
// cut and in nothing else. Called only by
// MG_Record::PersistentMapTracker, which owns the cutting.
void PushMappedSpanBlock(SizeT offset, SizeT size);
// Called on every event that can move the tracker's membership predicate: map,
// unmap, respecify, adoption, destruction. It is one call rather than an
// insert/erase pair on purpose - the predicate is read from this object, so a
// caller that had to decide which of the two to call could decide differently
// from IsLivePersistentMap and the set would drift from the thing it models.
//
// It also PUBLISHES the live-host-writes bit when it changes: the server must
// know that a write map is live, because such a map mutates the shadow with no
// call, no serial and no epoch, and the applier's IsBufferDrawCleanByHandle can
// no longer ask this object (there is no object on that side of a spawn).
void NotePersistentMapStateChanged();
// Keeps this buffer's membership in the live persistent READ map set current (see
// SyncPersistentReadMapsFromGpu). Called from NotePersistentMapStateChanged, so it
// rides every event that can move the predicate, on every transport.
void NotePersistentReadMapStateChanged();
// What MGPipeEmitResourceSubData and MGPipeEmitBufferSubDataResident write into
// MGPSubData::HasLiveHostWrites. The PUBLISHED value, not the live predicate: the
// two are the same by the time any content record is built, and reading the
// published one is what makes a record and the edge that announced it agree by
// construction.
Bool HasLiveHostWritesForWire() const;
// Is a GPU write still unreconciled? Under split this is the THIRD STATE made
// readable: SyncGpuWrites no longer clears optimistically, so between the readback
// emission and OnBufferWriteback landing this stays true, and nothing else in the
// object can express that. Split-only so the pull build's layout and inlining do
// not move (G1).
Bool HasOutstandingGpuWrite() const { return m_gpuWritePending; }
// The shadow allocation's own extent in bytes - page-aligned base, page-granular
// size, every byte this shadow's and nobody else's (PipeResource.h,
// ShadowAllocationBytesFor). Read by the persistent-map tracker at registration
// so it can protect the mapped range's containing pages OUTWARD and hash no
// edges; zero for an adopted store, which the tracker never registers. Split-only
// for G1's reason, like the rest of this block, and out of line because it is a
// layer-1 read of client memory like MappedData (CONTRACT-P5C rule E) and carries
// the same apply-thread refusal.
SizeT ShadowAllocationBytes() const;
Bool IsMapped() const;
Bool IsImmutableStorage() const;
SizeT GetSize() const;
BufferUsage GetUsage() const;
Range1D GetMappedRange() const;
void* GetMappedPointer() const;
// Host-visible base pointer to the buffer's authoritative bytes for
// [0, GetSize()): the coherent persistent GPU map when the buffer is
// persistent-resident, otherwise the CPU shadow. Every reader goes through
// this so no consumer branches on where the bytes live (the class of bug
// that a partial persistent-map redirect would reintroduce).
const Uint8* MappedData() const;
// True once the buffer's bytes were adopted into backend GPU memory (a
// coherent persistent map): reads/writes hit GPU memory and no per-write
// backend transfer op is dispatched.
Bool IsBackendPersistentMapped() const;
Flags<BufferMappingAccessBit> GetMappingAccess() const;
GLbitfield GetStorageFlags() const;
Uint GetExternalIndex() const;
// Globally-unique, never-reused id for THIS object's lifetime - same contract
// and same motivation as ProgramObject::GetLifetimeId() and
// VertexArrayObject::GetLifetimeId(). A backend that folds a buffer's IDENTITY
// into a cache key must use this, never the GL name (LIFO-recycled by
// glGenBuffers) and never the heap address (recycled by the allocator): both
// let a deleted-and-recreated buffer answer to a dead one's cache entry.
Uint64 GetLifetimeId() const { return m_lifetimeId; }
// P15: the pipe client's last Buffer handle for this object (MGPipeSlotAllocator::
// AcquireHinted). Only a hint: it is checked against the allocator on every use.
Uint64& PipeHandleHint() const { return m_pipeHandleHint; }
// Monotonic counter bumped on every shadow mutation; backends use it to
// validate cached transient slices.
Uint64 GetChangeSerial() const;
// False after a NULL-data (re)specification until the first content
// write: the app's orphaning idiom (glBufferData with nullptr) leaves
// the store undefined, so backends may (re)allocate GPU storage without
// uploading the stale CPU shadow.
Bool HasDefinedContent() const;
private:
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
// mapping across it. See the definition for why the renewal is not optional.
void RedefineStorage(SizeT size);
// Backend-initiated coherent adoption for mesh-arena-sized stores; see the
// definition for the driver behavior that makes every other write route to
// a busy large mutable store a frame-scale stall.
void TryAdoptLargeStorage();
void NotifyRespecify();
void NotifySubData(SizeT offset, SizeT size);
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
// A content write of [offset, offset+size) just landed in m_resource. For a
// persistent GPU-resident buffer the bytes are already in coherent GPU memory,
// so this only bumps the change serial; otherwise it dispatches a backend
// SubData transfer to sync the backend's separate GPU copy.
void NotifyContentWrite(SizeT offset, SizeT size);
// The one route CPU-sourced bytes take into an ADOPTED (GPU-resident) store:
// glBufferSubData, a buffer clear, a buffer copy, and the landing of a
// non-persistent write map at unmap / explicit flush all go through it, so
// the routes cannot drift apart again. Carries no mapping asserts on
// purpose - the unmap landing runs while the buffer is still mapped.
void LandBytesIntoResidentStore(SizeT offset, DataPtr bytes);
static Uint64 AllocateLifetimeId();
const Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
mutable Uint64 m_pipeHandleHint = 0;
SizeT m_size = 0;
BufferUsage m_usage = BufferUsage::StaticDraw;
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
// the backend GPU resource. All data access goes through it.
PipeResource m_resource;
Bool m_isMapped;
Flags<BufferMappingAccessBit> m_mappingAccess;
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
Uint64 m_changeSerial = 0;
// See HasDefinedContent().
Bool m_hasDefinedContent = true;
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed -
// and, in a split build, cleared by WritebackFromBackend instead, because there
// the answer arrives later than the request. See SyncGpuWrites' definition.
Bool m_gpuWritePending = false;
// The command seq the latest GPU-write mark was taken after. A monotonic record
// seq, never a count of writebacks: only a writeback produced by a LATER record
// can retire the mark (WritebackFromBackend).
Uint64 m_gpuWriteMarkSeq = 0;
// The last value of MGPResourceDesc::HasLiveHostWrites this object published.
// Behind the option so the pull build's layout - and therefore every inlined
// constructor and accessor in it - does not move (G1).
Bool m_publishedLiveHostWrites = false;
Range1D m_mappedRange;
// The write-map staging store. MapAlignedData because the application is handed a
// pointer into it, and biased by m_stagingBias because ARB_map_buffer_alignment
// requires (returned pointer - offset) to be aligned, not the pointer itself: a range
// map at offset 63 must hand back a pointer sitting 63 bytes past the alignment grid.
// The bias is the offset's phase, so the mapped bytes still start at
// m_stagingData.data() + m_stagingBias and the allocation is that much longer.
MapAlignedData m_stagingData;
SizeT m_stagingBias = 0;
Bool m_ownsStagingData;
};
// GL 4.6 core 6.2 / 7.13: GPU writes to a buffer become visible through a persistent
// READ map once the client has waited for them - glFinish, or a glClientWaitSync on a
// fence issued after them (coherent maps; a non-coherent map adds a
// CLIENT_MAPPED_BUFFER_BARRIER_BIT barrier before that fence). The pointer such a map
// hands out is the frontend shadow, which nothing else pulls the GPU's bytes into, so
// these two waits refresh every live persistent read map that has a GPU write
// outstanding. A CLIENT_MAPPED_BUFFER_BARRIER_BIT barrier refreshes them too, for an
// application that reads after some other wait instead of a fence.
void SyncPersistentReadMapsFromGpu();
} // namespace MG_State::GLState
} // namespace MobileGL