[WIP] Mesa-style buffer overhaul: resource abstraction + immediate transfer ops

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 14:47:51 +08:00
co-authored by Claude Fable 5
parent 2ed96e9678
commit b8ffd25148
15 changed files with 1048 additions and 642 deletions
@@ -9,24 +9,67 @@
#include "BufferObject.h"
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_change(BufferChangeBits::DirtyBit | BufferChangeBits::PreferReallocationBit), m_mappedRange({0, 0}),
m_dataPtr(MakeShared<Data>()), m_ownsStagingData{} {
m_change.DirtyRanges.reserve(BufferChange::DEFAULT_RESERVED_DIRTY_RANGES_COUNT);
namespace {
const BufferBackendOps* g_bufferBackendOps = nullptr;
}
void BufferObject::Resize(SizeT size) {
void SetBufferBackendOps(const BufferBackendOps* ops) {
g_bufferBackendOps = ops;
}
const BufferBackendOps* GetBufferBackendOps() {
return g_bufferBackendOps;
}
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_dataPtr(MakeShared<Data>()),
m_ownsStagingData{} {}
BufferObject::~BufferObject() {
if (m_backendResource && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) {
g_bufferBackendOps->OnDestroy(std::move(m_backendResource));
}
}
void BufferObject::NotifyRespecify() {
++m_changeSerial;
if (g_bufferBackendOps && g_bufferBackendOps->Respecify) {
g_bufferBackendOps->Respecify(*this);
}
}
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
++m_changeSerial;
if (size == 0) return;
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
g_bufferBackendOps->SubData(*this, offset, size);
}
}
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
++m_changeSerial;
if (range.start >= range.end) return;
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
}
}
void BufferObject::Respecify(SizeT size, const void* data) {
ReleaseMemory();
m_size = size;
m_dataPtr->reserve(std::bit_ceil(size)); // power-of-2 reserve
m_dataPtr->resize(size);
if (data && size > 0) {
Memcpy(m_dataPtr->data(), data, size);
}
m_isImmutableStorage = false;
m_storageFlags = 0;
m_change.Bits |= BufferChangeBits::DirtyBit;
m_change.Bits |= BufferChangeBits::PreferReallocationBit;
++m_changeSerial;
NotifyRespecify();
}
void BufferObject::Resize(SizeT size) {
Respecify(size, nullptr);
}
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
@@ -41,10 +84,7 @@ namespace MobileGL::MG_State::GLState {
}
m_isImmutableStorage = true;
m_storageFlags = storageFlags;
m_change.DirtyRanges.clear();
m_change.DirtyRanges.Add({0, size});
m_change.Bits = BufferChangeBits::DirtyBit | BufferChangeBits::PreferReallocationBit;
++m_changeSerial;
NotifyRespecify();
}
void BufferObject::UploadData(DataPtr data, SizeT atOffset) {
@@ -54,15 +94,7 @@ namespace MobileGL::MG_State::GLState {
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
"Cannot upload data while buffer is non-persistently mapped.");
Memcpy(m_dataPtr->data() + atOffset, data.data, data.size);
m_change.DirtyRanges.Add({atOffset, atOffset + data.size});
m_change.Bits |= BufferChangeBits::DirtyBit;
m_change.Bits |= BufferChangeBits::ForbidInvalidationBit;
m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit;
++m_changeSerial;
// This function may be called by `glBufferData`, but we still set the forbid bits above,
// because when `PreferReallocationBit` is set, those bits are ignored anyway.
// The bits can fit the `glBufferSubData` semantics
// (though `glBufferSubData` calls `UploadSubData` instead).
NotifySubData(atOffset, data.size);
}
void BufferObject::SetUsage(BufferUsage usage) {
@@ -78,9 +110,7 @@ namespace MobileGL::MG_State::GLState {
Memcpy(m_dataPtr->data() + m_mappedRange.start, m_stagingData.data(),
m_mappedRange.end - m_mappedRange.start);
}
m_change.DirtyRanges.Add({m_mappedRange.start, m_mappedRange.end});
m_change.Bits |= BufferChangeBits::DirtyBit;
++m_changeSerial;
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
}
m_stagingData.clear();
@@ -107,20 +137,24 @@ namespace MobileGL::MG_State::GLState {
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
Memcpy(m_dataPtr->data() + start, m_stagingData.data() + offset, length);
}
m_change.DirtyRanges.Add({start, end});
m_change.Bits |= BufferChangeBits::DirtyBit;
++m_changeSerial;
NotifyFlushMappedRange({start, end}, m_mappingAccess);
}
void BufferObject::MarkPersistentMappedRangeDirty() {
void BufferObject::SyncPersistentMappedRange() {
if (!m_isMapped) return;
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return;
if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return;
if (m_mappingAccess & BufferMappingAccessBit::FlushExplicit) return;
if (m_mappedRange.start >= m_mappedRange.end) return;
m_change.DirtyRanges.Add(m_mappedRange);
m_change.Bits |= BufferChangeBits::DirtyBit;
NotifySubData(m_mappedRange.start, m_mappedRange.end - m_mappedRange.start);
}
void BufferObject::WritebackFromBackend(DataPtr data, SizeT atOffset) {
MOBILEGL_ASSERT(atOffset + data.size <= m_size,
"WritebackFromBackend out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
data.size, m_size);
Memcpy(m_dataPtr->data() + atOffset, data.data, data.size);
++m_changeSerial;
}
@@ -132,11 +166,7 @@ namespace MobileGL::MG_State::GLState {
data.size, m_size);
Memcpy(m_dataPtr->data() + atOffset, data.data, data.size);
m_change.DirtyRanges.Add({atOffset, atOffset + data.size});
m_change.Bits |= BufferChangeBits::DirtyBit;
m_change.Bits |= BufferChangeBits::ForbidInvalidationBit;
m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit;
++m_changeSerial;
NotifySubData(atOffset, data.size);
}
void BufferObject::CopyDataFrom(const SharedPtr<BufferObject>& src, SizeT srcOffset, SizeT dstOffset, SizeT size) {
@@ -153,9 +183,7 @@ namespace MobileGL::MG_State::GLState {
const Uint8* srcData = src->m_dataPtr->data() + srcOffset;
Memcpy(m_dataPtr->data() + dstOffset, srcData, size);
m_change.DirtyRanges.Add({dstOffset, dstOffset + size});
m_change.Bits |= BufferChangeBits::DirtyBit;
++m_changeSerial;
NotifySubData(dstOffset, size);
}
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
@@ -164,10 +192,6 @@ namespace MobileGL::MG_State::GLState {
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
(write ? BufferMappingAccessBit::Write : BufferMappingAccessBit::Null);
m_mappedRange = {0, m_size};
if (write) {
m_change.Bits |= BufferChangeBits::ForbidInvalidationBit;
m_change.Bits |= BufferChangeBits::ForbidUnsynchronizationBit;
}
if (m_mappingAccess & BufferMappingAccessBit::Write) {
m_stagingData.resize(m_size);
@@ -192,21 +216,6 @@ namespace MobileGL::MG_State::GLState {
m_isMapped = true;
m_mappingAccess = access;
m_mappedRange = range;
m_change.Bits |=
!(access & BufferMappingAccessBit::InvalidateBuffer || access & BufferMappingAccessBit::InvalidateRange)
? BufferChangeBits::ForbidInvalidationBit
: BufferChangeBits::None;
m_change.Bits |= !(access & BufferMappingAccessBit::Unsynchronized)
? BufferChangeBits::ForbidUnsynchronizationBit
: BufferChangeBits::None;
m_change.Bits |=
!(access & BufferMappingAccessBit::InvalidateBuffer || access & BufferMappingAccessBit::InvalidateRange)
? BufferChangeBits::ForbidInvalidationBit
: BufferChangeBits::None;
m_change.Bits |= !(access & BufferMappingAccessBit::Unsynchronized)
? BufferChangeBits::ForbidUnsynchronizationBit
: BufferChangeBits::None;
if (access & BufferMappingAccessBit::Persistent) {
m_ownsStagingData = false;
@@ -232,11 +241,6 @@ namespace MobileGL::MG_State::GLState {
return m_dataPtr;
}
void BufferObject::ClearDirty() {
m_change.DirtyRanges.clear();
m_change.Bits = BufferChangeBits::None;
}
SizeT BufferObject::GetSize() const {
return m_size;
}
@@ -249,18 +253,18 @@ namespace MobileGL::MG_State::GLState {
return m_usage;
}
const VecRange1D& BufferObject::GetDirtyRanges() const {
return m_change.DirtyRanges;
}
Flags<BufferChangeBits> BufferObject::GetChangeBits() const {
return m_change.Bits;
}
Uint64 BufferObject::GetChangeSerial() const {
return m_changeSerial;
}
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
return m_backendResource;
}
void BufferObject::SetBackendResource(SharedPtr<BackendBufferResource> resource) {
m_backendResource = std::move(resource);
}
Bool BufferObject::IsMapped() const {
return m_isMapped;
}
@@ -56,42 +56,81 @@ namespace MobileGL {
Coherent = 0x80
};
enum class BufferChangeBits : Uint8 {
None = 0,
DirtyBit = 1 << 0, // When not set, bits below are ignored and nothing should be synced to backend
PreferReallocationBit =
1 << 1, // <=> `glBufferData`; When set, ForbidInvalidationBit and ForbidUnsynchronizationBit are ignored
ForbidInvalidationBit = 1 << 2, // Indidate that invalidation flags were not used during mapping, else we're
// allowed to act as `GL_MAP_INVALIDATE_*` in backend
ForbidUnsynchronizationBit = 1 << 3, // (the same description as above, but for unsynchronization)
};
struct BufferChange {
static constexpr int DEFAULT_RESERVED_DIRTY_RANGES_COUNT = 50;
Flags<BufferChangeBits> Bits = BufferChangeBits::None;
VecRange1D DirtyRanges;
};
namespace MG_State::GLState {
class BufferObject;
// Opaque, refcounted handle to the backend's storage for one buffer
// (the pipe_resource analogue). The frontend owns the reference; the
// active backend derives from it and attaches its own payload.
class BackendBufferResource {
public:
virtual ~BackendBufferResource() = default;
};
// 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<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;
};
// 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 UploadData(DataPtr data, SizeT atOffset);
void SetUsage(BufferUsage usage);
void UploadData(DataPtr data, SizeT atOffset);
void UploadSubData(DataPtr data, SizeT atOffset);
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);
void ReleaseMemory();
void FlushMemoryRange(SizeT offset, SizeT length);
void MarkPersistentMappedRangeDirty();
void UploadSubData(DataPtr data, SizeT atOffset);
void CopyDataFrom(const SharedPtr<BufferObject>& src, SizeT srcOffset, SizeT dstOffset, SizeT size);
void ClearDirty();
// 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);
Bool IsMapped() const;
Bool IsImmutableStorage() const;
@@ -103,11 +142,18 @@ namespace MobileGL {
Flags<BufferMappingAccessBit> GetMappingAccess() const;
GLbitfield GetStorageFlags() const;
Uint GetExternalIndex() const;
const VecRange1D& GetDirtyRanges() const;
Flags<BufferChangeBits> GetChangeBits() const;
// Monotonic counter bumped on every shadow mutation; backends use it to
// validate cached transient slices.
Uint64 GetChangeSerial() const;
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
private:
void NotifyRespecify();
void NotifySubData(SizeT offset, SizeT size);
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
const Uint m_externalIndex = 0;
SizeT m_size = 0;
BufferUsage m_usage = BufferUsage::StaticDraw;
@@ -116,11 +162,11 @@ namespace MobileGL {
Flags<BufferMappingAccessBit> m_mappingAccess;
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
BufferChange m_change;
Uint64 m_changeSerial = 0;
Range1D m_mappedRange;
Vector<Uint8> m_stagingData;
Bool m_ownsStagingData;
SharedPtr<BackendBufferResource> m_backendResource;
};
} // namespace MG_State::GLState
} // namespace MobileGL