mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +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 VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) {
|
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 VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
return it->second;
|
return *it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexInputStateBuilder builder;
|
VertexInputStateBuilder builder;
|
||||||
@@ -172,7 +184,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const auto& state = builder.Build();
|
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.hash = hash;
|
||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
@@ -221,8 +237,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
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);
|
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 {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
HashType layoutHash = 0;
|
HashType layoutHash = 0;
|
||||||
// Frame boundary of the last cache hit; entries idle past the
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
// 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<VkVertexInputBindingDescription> bindings;
|
||||||
Vector<VkVertexInputAttributeDescription> attributes;
|
Vector<VkVertexInputAttributeDescription> attributes;
|
||||||
Vector<SizeT> bindingBufferKeys;
|
Vector<SizeT> bindingBufferKeys;
|
||||||
@@ -87,9 +89,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
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.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
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();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -233,6 +233,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// points where this becomes unknown.
|
// points where this becomes unknown.
|
||||||
Bool graphicsPipelineValid = false;
|
Bool graphicsPipelineValid = false;
|
||||||
VkPipeline graphicsPipeline = VK_NULL_HANDLE;
|
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;
|
Bool viewportValid = false;
|
||||||
VkViewport viewport{};
|
VkViewport viewport{};
|
||||||
Bool scissorValid = false;
|
Bool scissorValid = false;
|
||||||
@@ -3183,8 +3196,29 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bindingCount > 0) {
|
if (bindingCount > 0) {
|
||||||
vkCmdBindVertexBuffers(commandBuffer, 0, static_cast<Uint32>(bindingCount), vkBuffers.data(),
|
auto& shadow = g_dynamicStateShadow;
|
||||||
vkOffsets.data());
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3254,8 +3288,17 @@ void main() {
|
|||||||
MGLOG_E("DrawElements skipped: failed to sync resident index buffer");
|
MGLOG_E("DrawElements skipped: failed to sync resident index buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer,
|
const VkDeviceSize indexBindOffset =
|
||||||
slice.offset + static_cast<VkDeviceSize>(pIndexBufferView->indexByteOffset), vkIndexType);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3823,11 +3866,11 @@ void main() {
|
|||||||
// vertex-input hash (VAO layout), render-pass hash (render targets + the draw-buffer/format
|
// 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).
|
// 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.
|
// 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
|
// 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
|
// one per buffer); the memo and the pipeline payload key on the resolved
|
||||||
// LAYOUT hash instead, so draws over identical layouts share one pipeline.
|
// 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 vertexLayoutHash = vis.layoutHash;
|
||||||
const Uint64 renderPassHash = renderPassEntry.hash;
|
const Uint64 renderPassHash = renderPassEntry.hash;
|
||||||
const Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
const Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||||
|
|||||||
@@ -104,6 +104,23 @@ namespace MobileGL {
|
|||||||
m_backendHashMemoVersion = m_configVersion;
|
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:
|
private:
|
||||||
void BumpAttributeFormatVersion(Uint index);
|
void BumpAttributeFormatVersion(Uint index);
|
||||||
void BumpAttributeBufferVersion(Uint index);
|
void BumpAttributeBufferVersion(Uint index);
|
||||||
@@ -137,6 +154,9 @@ namespace MobileGL {
|
|||||||
Uint32 m_configVersion = 0;
|
Uint32 m_configVersion = 0;
|
||||||
mutable Uint64 m_backendHashMemo = 0;
|
mutable Uint64 m_backendHashMemo = 0;
|
||||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||||
|
mutable const void* m_backendStateMemo = nullptr;
|
||||||
|
mutable Uint64 m_backendStateMemoEpoch = 0;
|
||||||
|
mutable Uint32 m_backendStateMemoVersion = ~0u;
|
||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
|
|||||||
Reference in New Issue
Block a user