mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Perf] (DirectVulkan): memoize resolved vertex-input state on the VAO and dedupe vertex/index binds
This commit is contained in:
@@ -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<const BackendVertexInputState*>(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>();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<VkVertexInputBindingDescription> bindings;
|
||||
Vector<VkVertexInputAttributeDescription> attributes;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
@@ -87,9 +89,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, BackendVertexInputState> 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<HashType, UniquePtr<BackendVertexInputState>> 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
|
||||
|
||||
@@ -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<Uint32>(bindingCount), vkBuffers.data(),
|
||||
vkOffsets.data());
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
const Uint32 count = static_cast<Uint32>(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<VkDeviceSize>(pIndexBufferView->indexByteOffset), vkIndexType);
|
||||
const VkDeviceSize indexBindOffset =
|
||||
slice.offset + static_cast<VkDeviceSize>(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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user