[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
@@ -295,6 +295,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Called once the storage-buffer points are bound and the draw/dispatch is about to
// go out: whatever the shader writes there lands in the ES driver's buffers, behind
// the frontend's CPU shadow. Flagging them makes the next MapBuffer/GetBufferSubData
// pull the real contents back (BufferObject::SyncGpuWrites).
void MarkShaderStorageBuffersGpuWritten() {
const SizeT bindingPointCnt =
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
for (SizeT i = 0; i < bindingPointCnt; ++i) {
const auto& obj =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
if (obj) obj->MarkGpuWritten();
}
}
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -367,6 +381,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// BindCurrentProgramWithResources binds no SSBO points, so this is their sole draw-path
// binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers).
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
MarkShaderStorageBuffersGpuWritten();
}
void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) {
@@ -376,6 +391,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ProcessDeferredBufferReleases();
SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER);
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
MarkShaderStorageBuffersGpuWritten();
if (includeDispatchIndirectBuffer) {
SyncBoundBuffer(BufferTarget::DispatchIndirect, GL_DISPATCH_INDIRECT_BUFFER);
}
@@ -630,6 +630,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
// A shader wrote this buffer through a storage/atomic-counter binding, so the ES
// driver's copy is ahead of the frontend shadow. Pull the whole thing back so
// MapBuffer/GetBufferSubData/CopyBufferSubData see the real results.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (resource->persistentMapped) return; // shadow already IS the GPU storage
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
if (size == 0) return;
BindBufferId(TempBufferTarget, resource->id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
bufferObject.WritebackFromBackend({mapped, size}, 0);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
// The shadow now matches the backend byte for byte; without this the next
// draw would see a newer change serial and re-upload the readback over it.
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (!resource) return;
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
@@ -662,6 +688,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
};
} // namespace