[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
@@ -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