diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 3320d6da..83bf620b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -18,6 +18,64 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { + namespace { + struct WireVertexInputMemo { + struct Key { + Uint64 Offset; + Uint32 Stride; + Uint32 Divisor; + }; + Bool Valid = false; + Uint64 ContentSerial = 0; + Uint32 ActiveMask = 0; + Uint32 Start = 0; + Uint32 Count = 0; + Uint32 KeyCount = 0; + Array Keys{}; + VertexInputStateFactory::BackendVertexInputState State; + }; + } // namespace + + const VertexInputStateFactory::BackendVertexInputState* VertexInputStateFactory::ResolveWireVertexInput( + const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state, + Uint32 activeMask) const { + auto* memo = static_cast(elements.BackendMemo.get()); + if (memo == nullptr) { + auto created = std::make_shared(); + memo = created.get(); + elements.BackendMemo = Move(created); + } + // The window first: a location outside it is BuildWireVertexInput's refusal, and the + // keys below index the window. + Bool hit = memo->Valid && memo->ContentSerial == elements.ContentSerial && memo->ActiveMask == activeMask && + memo->Start == state.VertexBufferStart && memo->Count == state.VertexBufferCount; + if (hit) { + Uint32 n = 0; + for (Uint32 location = 0; location < elements.AttributeCount && hit; ++location) { + if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue; + const auto& buffer = state.VertexBuffers[location]; + const auto& key = memo->Keys[n++]; + hit = key.Offset == buffer.Offset && key.Stride == buffer.Stride && key.Divisor == buffer.Divisor; + } + if (hit) return &memo->State; + } + memo->Valid = false; + if (!BuildWireVertexInput(elements, state, activeMask, memo->State)) return nullptr; + Uint32 n = 0; + for (Uint32 location = 0; location < elements.AttributeCount; ++location) { + if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue; + const auto& buffer = state.VertexBuffers[location]; + memo->Keys[n++] = {buffer.Offset, buffer.Stride, buffer.Divisor}; + } + memo->KeyCount = n; + memo->ContentSerial = elements.ContentSerial; + memo->ActiveMask = activeMask; + memo->Start = state.VertexBufferStart; + memo->Count = state.VertexBufferCount; + memo->Valid = true; + return &memo->State; + } + Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const { // The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index 87ce495d..34419200 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -119,6 +119,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const; + // The same layout, kept on the record (MGPipeVertexElementsRecord::BackendMemo) and rebuilt + // only when the record's configuration, the program's active mask, the buffer window or an + // active attribute's buffer offset/stride/divisor moved - everything the build reads that + // can change. Null where BuildWireVertexInput would return false. + const BackendVertexInputState* ResolveWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, + const MG_Pipe::MGPipeApplierState& state, + Uint32 activeMask) const; private: static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index c9b1068f..fee8fe70 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -184,13 +184,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { } // namespace VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) { - const Uint64 key = WireBufferKey(res); - auto& lookup = m_wireBufferLookup[key % kWireBufferLookupEntries]; - if (lookup.resource != nullptr && lookup.key == key && lookup.eraseCount == m_wireBufferEraseCount) - return lookup.resource; - const auto found = m_wireBuffers.find(key); + const SizeT slot = res.Slot; + if (slot < m_wireBufferLookup.size()) { + const auto& lookup = m_wireBufferLookup[slot]; + if (lookup.resource != nullptr && lookup.gen == res.Gen) return lookup.resource; + } + const auto found = m_wireBuffers.find(WireBufferKey(res)); if (found == m_wireBuffers.end()) return nullptr; - lookup = {key, m_wireBufferEraseCount, &found->second}; + if (slot < kWireBufferLookupMaxSlots) { + if (slot >= m_wireBufferLookup.size()) m_wireBufferLookup.resize(slot + 1); + m_wireBufferLookup[slot] = {res.Gen, &found->second}; + } return &found->second; } @@ -998,7 +1002,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { DeferWireRelease(std::move(found->second.buffer), found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial) : found->second.lastUseSerial); // P11 B2, as Respecify - ++m_wireBufferEraseCount; + ForgetWireBufferLookup(found->first); m_wireBuffers.erase(found); PublishWireReclaimGauges(); } @@ -1170,7 +1174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the // records still hold, so nothing this arm minted outlives the count. if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch; - ++m_wireBufferEraseCount; + m_wireBufferLookup.clear(); m_wireBuffers.clear(); m_wireStoreCount = 0; m_wireStoreCountPeak = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 04b120de..df452c7a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -292,18 +292,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { IBufferCopyCommandProvider* m_copyProvider = nullptr; Vector> m_deferredBufferReleases; std::unordered_map m_wireBuffers; - // P14: a direct-mapped front for m_wireBuffers (every draw resolves each vertex, index and - // uniform buffer). Keyed on WireBufferKey ({slot, gen}, never reused) and believed only at - // the erase count it was taken at: a node never moves until erased, and every erase moves - // the count. + // P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and + // uniform buffer). An entry is believed only for the generation it was taken at ({slot, + // gen} is never reused), and the erase of that node clears it: a node never moves until + // erased. Slot-indexed rather than direct-mapped because a Minecraft frame draws from a + // few thousand live buffers, which a small hashed table mostly missed. struct WireBufferLookup { - Uint64 key = 0; - Uint64 eraseCount = 0; + Uint32 gen = 0; WireBufferResource* resource = nullptr; }; - static constexpr SizeT kWireBufferLookupEntries = 256; - Array m_wireBufferLookup{}; - Uint64 m_wireBufferEraseCount = 0; + static constexpr SizeT kWireBufferLookupMaxSlots = SizeT{1} << 20; + Vector m_wireBufferLookup; + void ForgetWireBufferLookup(Uint64 key) { + const SizeT slot = static_cast(key & 0xffffffffu); + if (slot < m_wireBufferLookup.size()) m_wireBufferLookup[slot] = {}; + } // See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above: // the whole point is that these entries do not wait for a frame slot to come round. Vector m_deferredWireReleases; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index 92aab5eb..d31ff4f6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -311,7 +311,6 @@ if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); - auto& input = scratch.input; // P7 wave 2 package C, CONTRACT-P7 ยง3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT. // // A false is now ONE reason and only one - `buffer-window`, an enabled attribute the @@ -324,9 +323,18 @@ // protocol error, not a shape the split path has yet to migrate, so it must not keep // counting against the census of unmigrated refusals (scripts/ci/fatal_census.py // counts the marker literally, which is also why no comment in this package spells it). - if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state, - programObj.activeVertexInputLocationMask, input)) - MagmaWireFatal("vertex-layout-buffer-window"); + // P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested + // preparation, which builds into its own scratch rather than under the outer draw's feet. + const VertexInputStateFactory::BackendVertexInputState* resolvedInput = nullptr; + if (&scratch == &m_wireDrawScratch) { + resolvedInput = m_vertexInputStateFactory->ResolveWireVertexInput(vao, state, + programObj.activeVertexInputLocationMask); + } else if (m_vertexInputStateFactory->BuildWireVertexInput(vao, state, + programObj.activeVertexInputLocationMask, scratch.input)) { + resolvedInput = &scratch.input; + } + if (resolvedInput == nullptr) MagmaWireFatal("vertex-layout-buffer-window"); + const auto& input = *resolvedInput; // The seven device/format reasons are now MASKED OUT of the vertex input state, and // this is the monolith arm's pre-flight for them, statement for statement // (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An