From 56a77348f63b2db6c890671651b270845a93826d Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 02:21:08 -0400 Subject: [PATCH] [MG_Remote, MG_State] (Disaggregated): inproc performance, round 2 - every persistent map is tracked, and the consumer walk goes away in steady state Follow-up to the performance package (cb06538c): on device the epoch skip never engaged, because the workload always has sub-page persistent maps and any untracked (hash-arm) member vetoed it - the full binding walk ran at every draw (~8% of the client thread at VD12), and the direct-scan alternative measured strictly worse (41-43 vs 58-62 fps: per-draw whole-range scans of every untracked member plus 64-slot tracker lookups per member). The two-part answer, after the membership map started caching the tracker slot pointer in the parent commit: - a sub-page range no longer falls back to the hash arm: it registers with an EMPTY interior (no protected pages, no bitmap) and hasEdges set, so its whole range is served by the two bounded edge hashes instead of vetoing the skip for every buffer in the process; - the shadow's allocation alignment is a full page (SHADOW_ALLOCATION_ALIGNMENT = 4096, GL_MIN_MAP_BUFFER_ALIGNMENT still answers 64): every page a shadow occupies is wholly its own, so the tracker can protect the interior without ever covering a neighbour allocation's byte, and edge spans - and the zero-page shape itself - virtually disappear from real workloads. Monolith shares the allocator and is unaffected (110-118 fps A/B, no behaviour-visible change). VD12 Minecraft, same scene: 63-65 fps (parent commit 60-66, with the skip now engaging every steady-state draw rather than never). GetBindingPoint and the binding walk no longer appear in the profile at all; XXH3 drops 6.1 -> 2.6% (the rest is the set-record dedup hash, a pre-existing P5 site). Lanes: unit 2188/2188, integration-split 110/111 with the one red again the monolith-inert GenerateMipmapDepthPixels environmental flake. --- .../MG_Remote/Client/PersistentMapTracker.cpp | 28 +++++++++++++------ .../GLState/BufferState/PipeResource.h | 17 +++++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp index 34834673..1b54326a 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp @@ -274,10 +274,17 @@ namespace MobileGL::MG_Remote::Client { // their blocks are pushed unconditionally at every push (at most two). const uintptr_t rawBegin = reinterpret_cast(shadow + rangeBegin); const uintptr_t rawEnd = reinterpret_cast(shadow + rangeEnd); - const uintptr_t base = (rawBegin + kPageBytes - 1) & - ~static_cast(kPageBytes - 1); - const uintptr_t end = rawEnd & ~static_cast(kPageBytes - 1); - if (end <= base) return false; // sub-page range: the hash arm has it + uintptr_t base = (rawBegin + kPageBytes - 1) & + ~static_cast(kPageBytes - 1); + uintptr_t end = rawEnd & ~static_cast(kPageBytes - 1); + // A SUB-PAGE range is not refused; it is registered with an EMPTY interior + // (no pages are protected, no bitmap is needed) and hasEdges set, so the + // whole range is served by the two edge hashes - bounded at 8 KB per push - + // instead of falling to the hash arm, where as an "untracked" member it + // would veto the epoch skip for every tracked buffer in the process and + // force the full binding walk at every draw (measured on device). end is + // clamped up to base so the [base, end) arithmetic below never underflows. + if (end < base) end = base; const SizeT pageCount = (end - base) >> kPageShift; if (pageCount > kMaxTrackedPages) { MGLOG_W("MGPipe: persistent write map of %zu pages exceeds the mprotect " @@ -295,7 +302,7 @@ namespace MobileGL::MG_Remote::Client { } // Claimed. From here until the final base store the slot reads as // (base=1, end=0), which the handler skips on both tests. - if (g_trackedMaps[i].pageBits == nullptr) { + if (pageCount > 0 && g_trackedMaps[i].pageBits == nullptr) { g_trackedMaps[i].pageBits = new std::atomic[kPageBitsWords]; } g_trackedMaps[i].pageCount = pageCount; @@ -321,7 +328,8 @@ namespace MobileGL::MG_Remote::Client { bitsInWord == 64 ? ~0ull : ((1ull << bitsInWord) - 1), std::memory_order_relaxed); } - if (mprotect(reinterpret_cast(base), end - base, PROT_READ) != 0) { + if (end > base && + mprotect(reinterpret_cast(base), end - base, PROT_READ) != 0) { g_trackedMaps[i].base.store(0, std::memory_order_release); return false; } @@ -353,7 +361,9 @@ namespace MobileGL::MG_Remote::Client { g_trackedMaps[i].base.store(0, std::memory_order_release); g_trackedMaps[i].end.store(0, std::memory_order_release); g_trackedMaps[i].lifetimeId = 0; - mprotect(reinterpret_cast(base), end - base, PROT_READ | PROT_WRITE); + if (end > base) { + mprotect(reinterpret_cast(base), end - base, PROT_READ | PROT_WRITE); + } return; } } @@ -369,7 +379,9 @@ namespace MobileGL::MG_Remote::Client { g_trackedMaps[i].base.store(0, std::memory_order_release); g_trackedMaps[i].end.store(0, std::memory_order_release); g_trackedMaps[i].lifetimeId = 0; - mprotect(reinterpret_cast(base), end - base, PROT_READ | PROT_WRITE); + if (end > base) { + mprotect(reinterpret_cast(base), end - base, PROT_READ | PROT_WRITE); + } } } diff --git a/MobileGL/MG_State/GLState/BufferState/PipeResource.h b/MobileGL/MG_State/GLState/BufferState/PipeResource.h index e0d8ca96..3bd6f1a0 100644 --- a/MobileGL/MG_State/GLState/BufferState/PipeResource.h +++ b/MobileGL/MG_State/GLState/BufferState/PipeResource.h @@ -27,7 +27,18 @@ namespace MobileGL::MG_State::GLState { // MG_Util/ShaderTranspiler/Types.h. inline constexpr SizeT MIN_MAP_BUFFER_ALIGNMENT = 64; - // Allocator that gives every allocation MIN_MAP_BUFFER_ALIGNMENT. Deliberately minimal: the + // The shadow's ALLOCATION alignment is a full page, not the advertised 64: a + // page-aligned shadow owns every byte of every page it occupies, which is what lets + // the persistent-map mprotect tracker write-protect the mapped interior without ever + // covering a neighbour allocation's byte (a foreign thread with signals blocked + // faulting on a shared page is a process kill). GL_MIN_MAP_BUFFER_ALIGNMENT keeps + // answering 64 - over-aligning an allocation is invisible to the application, and a + // glMapBufferRange base that is page-aligned is 64-aligned. The cost is allocator + // rounding on small buffers, paid so the tracker never has to make the unsafe choice. + inline constexpr SizeT SHADOW_ALLOCATION_ALIGNMENT = 4096; + + // Allocator that gives every allocation SHADOW_ALLOCATION_ALIGNMENT. Deliberately + // minimal: the // vectors it backs hold raw bytes and are only ever sized, so allocate/deallocate plus the // rebinding and equality boilerplate std::vector requires is the whole interface. template @@ -41,10 +52,10 @@ namespace MobileGL::MG_State::GLState { T* allocate(SizeT count) { if (count == 0) return nullptr; return static_cast( - ::operator new(count * sizeof(T), std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT})); + ::operator new(count * sizeof(T), std::align_val_t{SHADOW_ALLOCATION_ALIGNMENT})); } void deallocate(T* pointer, SizeT) noexcept { - ::operator delete(pointer, std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT}); + ::operator delete(pointer, std::align_val_t{SHADOW_ALLOCATION_ALIGNMENT}); } template