[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.
This commit is contained in:
2026-09-18 02:21:08 -04:00
parent cb06538c06
commit 56a77348f6
2 changed files with 34 additions and 11 deletions
@@ -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<uintptr_t>(shadow + rangeBegin);
const uintptr_t rawEnd = reinterpret_cast<uintptr_t>(shadow + rangeEnd);
const uintptr_t base = (rawBegin + kPageBytes - 1) &
~static_cast<uintptr_t>(kPageBytes - 1);
const uintptr_t end = rawEnd & ~static_cast<uintptr_t>(kPageBytes - 1);
if (end <= base) return false; // sub-page range: the hash arm has it
uintptr_t base = (rawBegin + kPageBytes - 1) &
~static_cast<uintptr_t>(kPageBytes - 1);
uintptr_t end = rawEnd & ~static_cast<uintptr_t>(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<Uint64>[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<void*>(base), end - base, PROT_READ) != 0) {
if (end > base &&
mprotect(reinterpret_cast<void*>(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<void*>(base), end - base, PROT_READ | PROT_WRITE);
if (end > base) {
mprotect(reinterpret_cast<void*>(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<void*>(base), end - base, PROT_READ | PROT_WRITE);
if (end > base) {
mprotect(reinterpret_cast<void*>(base), end - base, PROT_READ | PROT_WRITE);
}
}
}
@@ -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 <typename T>
@@ -41,10 +52,10 @@ namespace MobileGL::MG_State::GLState {
T* allocate(SizeT count) {
if (count == 0) return nullptr;
return static_cast<T*>(
::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 <typename U>