// 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 #include #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; // 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 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&& 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& src, SizeT srcOffset, SizeT dstOffset, SizeT size); void* AcquireMemory(Bool markMapped, Bool read, Bool write); void* AcquireMemoryRange(Range1D range, Flags 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 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& GetBackendResource() const; void SetBackendResource(SharedPtr 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); void NotifyRespecify(); void NotifySubData(SizeT offset, SizeT size); void NotifyFlushMappedRange(Range1D range, Flags 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 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