mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
Create 6 / Flywheel 1.0.6 now renders correctly with both flywheel:instancing and flywheel:indirect on DirectGLES and DirectVulkan (verified in-game on Adreno 830: waterwheels and cogwheels solid, animated, correct pairing, no crashes across all four combinations). - MG_State/MG_Impl: sync explicitly-ranged SSBO bindings of FLUSH_EXPLICIT persistent maps to the backend before compute dispatches. Flywheel writes its scatter-copy descriptors into the staging ring's persistent map and never flushes that span (UB per spec, works on drivers whose maps alias GPU-visible memory); our maps alias the CPU shadow, so the descriptors never reached the GPU: the scatter compute copied nothing (GLES: empty draw commands) or stale garbage (Vulkan: wild indirect commands ending in VK_ERROR_DEVICE_LOST). - MG_Impl/MG_Backend: real glFenceSync objects backed by backend fences (GLES: native ES syncs guarded by context generation and owner thread; Vulkan: buffer-manager frame serials), replacing always-signaled stubs that let Flywheel reclaim staging memory the GPU still reads. - MG_Backend/DirectGLES: compute dispatches now run the same per-program resource sync as draws (uniform-block bindings and sampler units must be re-established through the API because layout(binding) is stripped from transpiled ESSL) and rebind texture units afterwards; the cull shader used to read a stale _FlwFrameUniforms binding and the depth-pyramid downsample sampled a stale unit-0 texture, zeroing the Hi-Z pyramid and occlusion-culling all Flywheel geometry. Image uniforms are excluded from glUniform1i (ES bakes their unit via layout(binding)); image-unit sync is clamped to the device limit; eliminated/SSBO-classified uniform blocks are skipped. - MG_Backend/DirectGLES: gl_BaseInstance in native indirect draws reads the GPU-written command buffer through an injected mg_IndirectParams SSBO view addressed per draw instead of the zero CPU shadow; layout(binding) is preserved for SSBO/image declarations (ES has no API rebinding for them); the ES context ownership claim moved to a global atomic owner thread with an EGL ground-truth check, and deferred buffer op state is mutex-guarded, so ops cannot silently no-op after context migration. - MG_Backend/DirectVulkan: new RebaseInstanceIndexPass rewrites vertex InstanceIndex loads to (InstanceIndex - BaseInstance). glslang's relaxed Vulkan mode aliases gl_InstanceID to InstanceIndex, which includes firstInstance, but GL's gl_InstanceID is zero-based - draws with nonzero baseInstance paired meshes with wrong instance data (cogwheel drawn as a waterwheel, another wheel collapsed invisible). Gated on the shaderDrawParameters device feature. Sampled-read barriers additionally cover the compute stage (the Hi-Z downsample samples the depth attachment from compute), and short uniform-buffer ranges keep the existing zero-padding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
7.5 KiB
C++
179 lines
7.5 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>
|
|
|
|
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;
|
|
|
|
// 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 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);
|
|
|
|
// 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();
|
|
// App-compat for FLUSH_EXPLICIT persistent maps: engines like Flywheel
|
|
// write copy descriptors into the mapping and bind that range as an SSBO
|
|
// without ever flushing it. Undefined per spec, but works on drivers whose
|
|
// persistent maps alias GPU-visible memory. Ours alias the CPU shadow, so
|
|
// callers push the GPU-read range down right before it is consumed.
|
|
void SyncMappedRangeForGpuRead(Range1D range);
|
|
// 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;
|
|
SizeT GetSize() const;
|
|
BufferUsage GetUsage() const;
|
|
Range1D GetMappedRange() const;
|
|
void* GetMappedPointer() const;
|
|
const SharedPtr<Data>& GetDataReadOnly() const;
|
|
Flags<BufferMappingAccessBit> GetMappingAccess() const;
|
|
GLbitfield GetStorageFlags() const;
|
|
Uint GetExternalIndex() 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;
|
|
SharedPtr<Data> m_dataPtr;
|
|
Bool m_isMapped;
|
|
Flags<BufferMappingAccessBit> m_mappingAccess;
|
|
Bool m_isImmutableStorage = false;
|
|
GLbitfield m_storageFlags = 0;
|
|
Uint64 m_changeSerial = 0;
|
|
Range1D m_mappedRange;
|
|
Vector<Uint8> m_stagingData;
|
|
Bool m_ownsStagingData;
|
|
SharedPtr<BackendBufferResource> m_backendResource;
|
|
};
|
|
} // namespace MG_State::GLState
|
|
} // namespace MobileGL
|