mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
269 lines
14 KiB
C++
269 lines
14 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.
|
|
|
|
// Immediate buffer transfer interface implemented by the active backend
|
|
// (the pipe_context buffer-op analogue). Ops are invoked at GL call time,
|
|
// right after the shadow copy has been updated; contents are always read
|
|
// from the shadow so ops carry only ranges and flags.
|
|
//
|
|
// Every op must tolerate bufferObject.GetBackendResource() == nullptr:
|
|
// resources are created lazily by the backend's draw/bind-time ensure
|
|
// path, which performs a full upload from the shadow and thereby covers
|
|
// all ops that happened before the resource existed.
|
|
struct BufferBackendOps {
|
|
// Storage (re)definition: glBufferData / glBufferStorage. The orphaning
|
|
// point - the backend decides (busy-tracking) whether to swap storage
|
|
// or write in place. Shadow already holds the new contents.
|
|
void (*Respecify)(BufferObject& bufferObject) = nullptr;
|
|
// Contents update of [offset, offset + size) from the shadow.
|
|
void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr;
|
|
// Contents update of an ADOPTED (GPU-resident) store. `data` holds the app's
|
|
// bytes; the frontend has NOT touched the resident mapping. GL orders a
|
|
// glBufferSubData after already-submitted GPU reads of the store, and an
|
|
// in-place host write into the coherent mapping tears the frames still
|
|
// reading the old bytes (Minecraft patches LIVE chunk sections this way -
|
|
// the tear shows as one-frame wrong geometry/UVs during fast movement). The
|
|
// backend lands the bytes on the GPU timeline instead: after in-flight
|
|
// readers, before the next consumer. The frontend marks the buffer
|
|
// gpu-write-pending so reads reconcile through ReadbackFromGpu. Backends
|
|
// without this op keep the legacy ordered in-place host write.
|
|
void (*ResidentSubData)(BufferObject& bufferObject, SizeT offset, DataPtr data) = nullptr;
|
|
// Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the
|
|
// app's real mapping flags so the backend can honour INVALIDATE_* /
|
|
// UNSYNCHRONIZED semantics per call instead of merging them.
|
|
void (*FlushMappedRange)(BufferObject& bufferObject, Range1D range,
|
|
Flags<BufferMappingAccessBit> appAccess) = nullptr;
|
|
// Final release of the backend resource (called from ~BufferObject).
|
|
// The backend defers actual destruction until the GPU is done with it.
|
|
void (*OnDestroy)(SharedPtr<BackendBufferResource>&& resource) = nullptr;
|
|
// Zero-copy persistent mapping. For a coherent (non-FLUSH_EXPLICIT) persistent
|
|
// write map, the backend may hand back a host-visible, COHERENT, persistently
|
|
// mapped pointer into its own GPU storage for the whole buffer [0, size),
|
|
// created with every buffer usage and seeded from the shadow. From that point
|
|
// the GPU buffer is the single source of truth: the app writes into it
|
|
// directly, all reads/writes resolve against it (HostData()), and NO further
|
|
// backend transfer ops are dispatched for this buffer. Returns nullptr when the
|
|
// backend cannot back the map; the frontend then keeps the CPU-shadow model.
|
|
// Must be idempotent: a second call for an already-backed buffer returns the
|
|
// same base pointer.
|
|
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
|
|
// Pulls the backend's current contents for the whole buffer into the shadow
|
|
// (through WritebackFromBackend). Only ever called for a buffer the GPU may
|
|
// have written behind the frontend's back - a shader storage or atomic counter
|
|
// binding of a draw or dispatch - because nothing else can desynchronise the
|
|
// shadow. Backends that cannot read their storage back leave this null; the
|
|
// shadow then keeps its pre-dispatch bytes, which is the old behaviour.
|
|
void (*ReadbackFromGpu)(BufferObject& bufferObject) = nullptr;
|
|
};
|
|
|
|
// Registered by the active backend at init, cleared at shutdown.
|
|
// Null table (unit tests, benchmarks) => shadow-only state tracking.
|
|
void SetBufferBackendOps(const BufferBackendOps* ops);
|
|
const BufferBackendOps* GetBufferBackendOps();
|
|
|
|
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 or when the backend declines.
|
|
Bool EnsureGpuResidentStorage();
|
|
void ReleaseMemory();
|
|
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.
|
|
void WritebackFromBackend(DataPtr data, SizeT atOffset);
|
|
|
|
// 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();
|
|
// 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();
|
|
|
|
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; }
|
|
// 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;
|
|
|
|
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
|
|
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
|
|
|
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);
|
|
|
|
static Uint64 AllocateLifetimeId();
|
|
|
|
const Uint m_externalIndex = 0;
|
|
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
|
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.
|
|
Bool m_gpuWritePending = 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;
|
|
};
|
|
} // namespace MG_State::GLState
|
|
} // namespace MobileGL
|