From 8346d8099998efd461f872a8244927ae648c23bf Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 7 Oct 2026 10:27:00 -0400 Subject: [PATCH] [Perf] (MG_Pipe): lifetime-id slot lookups get a direct-mapped front and buffer binding slots are indexed (rd12 monolith +1.5%). --- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp | 11 +++++++++++ MobileGL/MG_Impl/Pipe/SlotAllocator.h | 11 +++++++++++ MobileGL/MG_State/GLState/BufferState/BufferState.cpp | 7 +++++++ MobileGL/MG_State/GLState/BufferState/BufferState.h | 2 ++ 4 files changed, 31 insertions(+) diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index 4ee5f313..006af2fa 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -206,10 +206,20 @@ namespace MobileGL::MG_Pipe { #endif if (lifetimeId == 0) return kMGPipeNullHandle; const KindState& state = StateOf(kind); + // The direct-mapped front (SlotAllocator.h): a hit is only believed when the slot it + // names is live AND still owned by this very lifetime id. + auto& recent = state.Recent[lifetimeId % KindState::kRecentEntries]; + if (recent.LifetimeId == lifetimeId) { + const SlotState* entry = EntryOf(state, kind, recent.Slot); + if (entry != nullptr && entry->Live && entry->LifetimeId == lifetimeId) + return MGPipeHandle{recent.Slot, entry->Gen}; + } const auto it = state.ByLifetimeId.find(lifetimeId); if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle; const SlotState* entry = EntryOf(state, kind, it->second); if (entry == nullptr || !entry->Live) return kMGPipeNullHandle; + recent.LifetimeId = lifetimeId; + recent.Slot = it->second; return MGPipeHandle{it->second, entry->Gen}; } @@ -304,6 +314,7 @@ namespace MobileGL::MG_Pipe { state.BandSlots.clear(); state.BandFreeList.clear(); state.ByLifetimeId.clear(); + state.Recent.fill({}); state.LiveCount = 0; state.BandLiveCount = 0; } diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index 1224ed97..a3a368c7 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -144,6 +144,17 @@ namespace MobileGL::MG_Pipe { Vector BandSlots; Vector BandFreeList; UnorderedMap ByLifetimeId; + // P14: a direct-mapped front for ByLifetimeId. Every draw resolves its VAO, its + // vertex buffers and its textures by lifetime id, and the hash map was ~2-3% of a + // Minecraft frame. Keyed on the lifetime id (monotonic, never reused) and confirmed + // against the slot's own LifetimeId and Live bit on every hit, so a stale entry can + // only miss. Mutable: FindByLifetimeId is const and fills it. + struct RecentEntry { + Uint64 LifetimeId = 0; + Uint32 Slot = 0; + }; + static constexpr SizeT kRecentEntries = 1024; + mutable Array Recent{}; Uint32 LiveCount = 0; // The band's share of LiveCount above, so the two spaces can be reported apart // without walking either table. Always 0 for every kind but ShaderCso. diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp index 697c9495..8a39dcab 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp @@ -10,8 +10,11 @@ namespace MobileGL::MG_State::GLState { BufferState::BufferState() { + m_bindingSlotIndex.fill(-1); for (SizeT i = 0; i < m_bindingSlots.size(); ++i) { m_bindingSlots[i] = BindingSlot(GlobalBufferTargets[i]); + const auto target = static_cast(GlobalBufferTargets[i]); + if (target < m_bindingSlotIndex.size()) m_bindingSlotIndex[target] = static_cast(i); } for (SizeT i = 0; i < m_touchedBindPointCount.size(); ++i) { m_touchedBindPointCount[i] = 0; @@ -19,6 +22,10 @@ namespace MobileGL::MG_State::GLState { } BindingSlot& BufferState::GetBindingSlot(BufferTarget target) { + // O(1) through the index the constructor built (every draw asks for several targets). + const auto index = static_cast(target); + if (index < m_bindingSlotIndex.size() && m_bindingSlotIndex[index] >= 0) + return m_bindingSlots[static_cast(m_bindingSlotIndex[index])]; for (auto& bindingSlot : m_bindingSlots) { if (bindingSlot.GetTarget() == target) { return bindingSlot; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.h b/MobileGL/MG_State/GLState/BufferState/BufferState.h index 0fafd33e..c3450908 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.h @@ -106,6 +106,8 @@ namespace MobileGL::MG_State::GLState { Uint64 m_anyBufferChangeGeneration = 0; Array m_bindPointGeneration{}; Array, GlobalBufferTargets.size()> m_bindingSlots; + // BufferTarget -> index into m_bindingSlots, -1 for a target with no global slot. + Array(BufferTarget::BufferTargetCount)> m_bindingSlotIndex{}; // TODO: query the count somewhere globally? // For glBindBufferBase / glBindBufferRange Array, BufferBindingPointCount>, BufferBindPointTargets.size()>