diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 0ebba699..e5d1028c 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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); } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index cf8a400f..b6170424 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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(bufferObject.GetSize(), resource->storageSize); + if (size == 0) return; + + BindBufferId(TempBufferTarget, resource->id); + void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(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&& resource) { if (!resource) return; auto* glesResource = static_cast(resource.get()); @@ -662,6 +688,7 @@ namespace MobileGL::MG_Backend::DirectGLES { .FlushMappedRange = Ops_FlushMappedRange, .OnDestroy = Ops_OnDestroy, .AcquirePersistentMap = Ops_AcquirePersistentMap, + .ReadbackFromGpu = Ops_ReadbackFromGpu, }; } // namespace diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp index d7472ca5..7f11e8cc 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp @@ -878,6 +878,7 @@ namespace MobileGL::MG_Impl::GLImpl { return; } + bufferObject->SyncGpuWrites(); bufferObject->DownloadSubData(data, static_cast(offset), static_cast(size)); } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index a69a462a..af971f34 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -174,6 +174,24 @@ namespace MobileGL::MG_State::GLState { ++m_changeSerial; } + void BufferObject::MarkGpuWritten() { + // A GPU-resident buffer has no separate shadow to refresh: reads already resolve + // against the coherent map the shader wrote into. + if (m_resource.IsGpuResident()) return; + m_gpuWritePending = true; + } + + void BufferObject::SyncGpuWrites() { + if (!m_gpuWritePending) return; + // Cleared unconditionally: without a readback op the shadow can never catch up, + // and retrying on every subsequent read would only repeat the same no-op. + m_gpuWritePending = false; + if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) { + return; + } + g_bufferBackendOps->ReadbackFromGpu(*this); + } + void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) { MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent), "Cannot upload sub data while buffer is non-persistently mapped."); @@ -204,11 +222,13 @@ namespace MobileGL::MG_State::GLState { "Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset, size, m_size); + src->SyncGpuWrites(); Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size); NotifyContentWrite(dstOffset, size); } void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) { + SyncGpuWrites(); if (markMapped) { m_isMapped = true; m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) | @@ -250,6 +270,10 @@ namespace MobileGL::MG_State::GLState { MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end, "AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start, range.end, m_size); + // The app is about to look at the bytes; a shader may have rewritten them since + // the shadow was last authoritative. Also needed for a write map without an + // invalidate bit, whose staging copy is seeded from the shadow. + SyncGpuWrites(); m_isMapped = true; m_mappingAccess = access; m_mappedRange = range; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index 6c441ea6..d6268970 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -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 m_stagingData; Bool m_ownsStagingData;