From afda613bd9b0376d9cb5a4024c2a5ef7166b4551 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 07:00:28 -0400 Subject: [PATCH] [Fix] (DirectGLES): publish GLESBufferResource::hostBytes under pendingMutex - it was the one member the off-thread SubData/FlushRange arm wrote with neither a lock nor an atomic --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index bea90a43..84e25af1 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1946,7 +1946,24 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT size = static_cast(MG_Pipe::MGPipeSubDataBufferSize(record)); auto* resource = FindBufferResourceForHandle(res); if (!resource) return; - if (bytes != nullptr) resource->hostBytes = static_cast(bytes) - offset; + // M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow() + // test below - i.e. on the arm D-A2 deliberately keeps reachable off the render + // thread - while every reader of hostBytes (Ops_H_Readback's drain, the + // kill-switch map arm of Ops_H_FlushRange, the ensure path, the fp64 narrowing) + // is on the render thread. It was the one member of this struct that the + // off-thread path touched with neither a lock nor an atomic: pendingRanges and + // pendingResidentWrites are under this mutex and syncedChangeSerial is an + // std::atomic read with acquire. The lock it is put under is deliberately THAT + // one and not a new one - the base and the queued range are one fact ("these + // bytes, at this base"), the drain takes this mutex to lift the ranges, and a + // drain that sees a range therefore sees the base that range was queued + // against. The store is kept on both arms rather than restricted to the + // CanTouchGLNow() one because Ops_H_Readback can drain with no draw in between, + // and the ensure path's republication (the 3c55e027 fix) is what runs at a draw. + if (bytes != nullptr) { + const std::lock_guard lock(resource->pendingMutex); + resource->hostBytes = static_cast(bytes) - offset; + } if (resource->pendingRespecify) return; // full re-upload pending anyway if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration || @@ -1994,7 +2011,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!resource) return; const SizeT start = static_cast(record.Offset); const SizeT end = start + static_cast(record.Size); - if (bytes != nullptr) resource->hostBytes = static_cast(bytes) - start; + // M-2, the second store site - same lock, same reason as Ops_H_SubData's. + if (bytes != nullptr) { + const std::lock_guard lock(resource->pendingMutex); + resource->hostBytes = static_cast(bytes) - start; + } if (resource->pendingRespecify) return; if (!CanTouchGLNow() || resource->id == 0 || resource->contextGeneration != g_bufferContextGeneration ||