[Fix] (MG_State, DirectGLES): read a shader-written storage buffer back before mapping it

Buffer contents live in a CPU shadow that every read - MapBuffer, MapBufferRange,
GetBufferSubData, CopyBufferSubData - resolves against, and backend transfer ops only
ever push the shadow outwards. Two paths already knew the GPU can write a buffer on
its own and mirrored the result back by hand (ReadPixels into a pixel-pack buffer,
the transform feedback capture at EndTransformFeedback); a shader storage buffer
written by a draw or a dispatch had no such path at all, so the map handed the
application the bytes from before the dispatch.

Nothing exercised it until now because GL 3.3 has no compute stage. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader that writes its
sampled texel into an SSBO and comparing the mapped result, and all 71 read back the
zero-filled shadow.

Adds the missing direction as a backend op: BufferObject::MarkGpuWritten flags a
buffer the GPU may have moved ahead of the shadow, SyncGpuWrites pulls it back at
every read point, and DirectGLES implements the readback with a plain read map of the
ES buffer. The flag is raised where the storage-buffer points are bound for the
upcoming draw or dispatch, which is the last moment the set of exposed buffers is
known, and cleared by the readback - so a buffer nothing writes costs one bool test
per map. Backends that cannot read their storage back leave the op null and keep
today's behaviour; a GPU-resident (coherent persistent) buffer needs nothing, since
its reads already resolve against the memory the shader wrote.

Drops the texture_gather failures from 71/75 to 25/75 with no crashes left.
This commit is contained in:
BZLZHH
2026-08-04 09:40:53 -04:00
parent 41e45f7d48
commit 9bf23d7ffd
5 changed files with 85 additions and 0 deletions
@@ -99,6 +99,13 @@ namespace MobileGL {
// 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.
@@ -149,6 +156,14 @@ namespace MobileGL {
// 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 GPU copy may now differ from the
// shadow, so the next read has to pull it back.
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;
@@ -196,6 +211,8 @@ namespace MobileGL {
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
Uint64 m_changeSerial = 0;
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
Bool m_gpuWritePending = false;
Range1D m_mappedRange;
Vector<Uint8> m_stagingData;
Bool m_ownsStagingData;