diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 1da72b7b..9893ffbe 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -58,15 +58,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const MG_State::GLState::VertexArrayObject& vao) { - return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao)); + // Per-draw fast path: the VAO carries a pointer to its resolved entry, + // valid while its config version and the cache's eviction epoch both + // match - no re-hash, no map lookup. + const void* memoState = nullptr; + Uint64 memoEpoch = 0; + if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) { + const auto* entry = static_cast(memoState); + entry->lastUsedFrameBoundary = m_frameBoundaryCounter; + return *entry; + } + const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao)); + vao.SetBackendStateMemo(&entry, m_evictionEpoch); + return entry; } const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const MG_State::GLState::VertexArrayObject& vao, HashType hash) { auto it = m_cache.find(hash); if (it != m_cache.end()) { - it->second.lastUsedFrameBoundary = m_frameBoundaryCounter; - return it->second; + it->second->lastUsedFrameBoundary = m_frameBoundaryCounter; + return *it->second; } VertexInputStateBuilder builder; @@ -172,7 +184,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto& state = builder.Build(); - auto& entry = m_cache[hash]; + auto& slot = m_cache[hash]; + if (!slot) { + slot = MakeUnique(); + } + BackendVertexInputState& entry = *slot; entry.hash = hash; entry.lastUsedFrameBoundary = m_frameBoundaryCounter; entry.bindings = builder.GetBindings(); @@ -221,8 +237,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { } for (auto it = m_cache.begin(); it != m_cache.end();) { - if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) { + if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) { it = m_cache.erase(it); + // Invalidate every VAO's state-pointer memo: the erased node's + // address may be reused by a future insert. + ++m_evictionEpoch; } else { ++it; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index eb89d6d3..3d12876f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -36,7 +36,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { HashType layoutHash = 0; // Frame boundary of the last cache hit; entries idle past the // OnFrameBoundary retirement age are evicted (CPU heap only). - Uint64 lastUsedFrameBoundary = 0; + // Mutable: the VAO's state-pointer memo fast path stamps it through + // a const entry reference. + mutable Uint64 lastUsedFrameBoundary = 0; Vector bindings; Vector attributes; Vector bindingBufferKeys; @@ -87,9 +89,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { const VulkanRendererConfig& m_config; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; - UnorderedMap m_cache; + // Values are heap-allocated: FastSTL::unordered_map is open-addressing, + // so INSERT invalidates references to stored values. The draw path (and + // the VAOs' state-pointer memos) hold entry pointers across inserts; + // only the unique_ptr cell moves, never the pointee. + UnorderedMap> m_cache; // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. Uint64 m_frameBoundaryCounter = 0; + // Bumped whenever any cache entry is erased. VAOs memo a raw pointer to + // their heap-allocated entry (stable across map insert/rehash by + // construction); a memo is honored only while its recorded epoch + // matches, so an evicted entry can never be dereferenced through a + // stale memo. + Uint64 m_evictionEpoch = 1; static inline XXH64_state_t* m_hashState = XXH64_createState(); }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 008c0234..c9a75543 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -233,6 +233,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // points where this becomes unknown. Bool graphicsPipelineValid = false; VkPipeline graphicsPipeline = VK_NULL_HANDLE; + // Index/vertex buffer binds are command-buffer state too. Terrain + // sections and GUI quads share one sequential index buffer, and GUI + // batches often reuse a vertex arena buffer, so skipping identical + // rebinds removes a large share of per-draw driver calls. + Bool indexBindValid = false; + VkBuffer indexBuffer = VK_NULL_HANDLE; + VkDeviceSize indexOffset = 0; + VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM; + static constexpr Uint32 kMaxShadowedVertexBindings = 8; + Bool vertexBindValid = false; + Uint32 vertexBindingCount = 0; + VkBuffer vertexBuffers[kMaxShadowedVertexBindings] = {}; + VkDeviceSize vertexOffsets[kMaxShadowedVertexBindings] = {}; Bool viewportValid = false; VkViewport viewport{}; Bool scissorValid = false; @@ -3183,8 +3196,29 @@ void main() { } if (bindingCount > 0) { - vkCmdBindVertexBuffers(commandBuffer, 0, static_cast(bindingCount), vkBuffers.data(), - vkOffsets.data()); + auto& shadow = g_dynamicStateShadow; + const Uint32 count = static_cast(bindingCount); + Bool identical = shadow.vertexBindValid && shadow.vertexBindingCount == count && + count <= DynamicStateShadow::kMaxShadowedVertexBindings; + if (identical) { + for (Uint32 i = 0; i < count; ++i) { + if (shadow.vertexBuffers[i] != vkBuffers[i] || shadow.vertexOffsets[i] != vkOffsets[i]) { + identical = false; + break; + } + } + } + if (!identical) { + vkCmdBindVertexBuffers(commandBuffer, 0, count, vkBuffers.data(), vkOffsets.data()); + if (count <= DynamicStateShadow::kMaxShadowedVertexBindings) { + shadow.vertexBindValid = true; + shadow.vertexBindingCount = count; + std::copy_n(vkBuffers.data(), count, shadow.vertexBuffers); + std::copy_n(vkOffsets.data(), count, shadow.vertexOffsets); + } else { + shadow.vertexBindValid = false; + } + } } return true; } @@ -3254,8 +3288,17 @@ void main() { MGLOG_E("DrawElements skipped: failed to sync resident index buffer"); return false; } - vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, - slice.offset + static_cast(pIndexBufferView->indexByteOffset), vkIndexType); + const VkDeviceSize indexBindOffset = + slice.offset + static_cast(pIndexBufferView->indexByteOffset); + auto& shadow = g_dynamicStateShadow; + if (!shadow.indexBindValid || shadow.indexBuffer != slice.buffer || + shadow.indexOffset != indexBindOffset || shadow.indexType != vkIndexType) { + vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, indexBindOffset, vkIndexType); + shadow.indexBindValid = true; + shadow.indexBuffer = slice.buffer; + shadow.indexOffset = indexBindOffset; + shadow.indexType = vkIndexType; + } return true; } @@ -3823,11 +3866,11 @@ void main() { // vertex-input hash (VAO layout), render-pass hash (render targets + the draw-buffer/format // driven blend & write-mask gating), and the render-state version (all fixed-function state). // Reset per-frame and on pipeline destruction so a memoized handle can never dangle. - const Uint64 vertexInputHash = m_vertexInputStateFactory->GetOrComputeHash(vao); // The identity hash mixes buffer heap addresses (per-chunk VBOs mint a new // one per buffer); the memo and the pipeline payload key on the resolved // LAYOUT hash instead, so draws over identical layouts share one pipeline. - auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao, vertexInputHash); + // The one-arg fetch rides the VAO's state-pointer memo (no hash, no map). + auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); const Uint64 vertexLayoutHash = vis.layoutHash; const Uint64 renderPassHash = renderPassEntry.hash; const Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion(); diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h index a7ee44f3..6d945103 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h @@ -104,6 +104,23 @@ namespace MobileGL { m_backendHashMemoVersion = m_configVersion; } + // Backend-owned resolved-state memo: an opaque pointer into the + // backend's vertex-input-state cache plus the cache's eviction + // epoch, valid while the config version matches. Lets the + // per-draw path skip the content hash AND the cache lookup; the + // epoch guards against the cache evicting the pointee. + Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const { + if (m_backendStateMemoVersion != m_configVersion) return false; + outState = m_backendStateMemo; + outEpoch = m_backendStateMemoEpoch; + return true; + } + void SetBackendStateMemo(const void* state, Uint64 epoch) const { + m_backendStateMemo = state; + m_backendStateMemoEpoch = epoch; + m_backendStateMemoVersion = m_configVersion; + } + private: void BumpAttributeFormatVersion(Uint index); void BumpAttributeBufferVersion(Uint index); @@ -137,6 +154,9 @@ namespace MobileGL { Uint32 m_configVersion = 0; mutable Uint64 m_backendHashMemo = 0; mutable Uint32 m_backendHashMemoVersion = ~0u; + mutable const void* m_backendStateMemo = nullptr; + mutable Uint64 m_backendStateMemoEpoch = 0; + mutable Uint32 m_backendStateMemoVersion = ~0u; }; } // namespace GLState } // namespace MG_State