mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Perf] (DirectVulkan): reuse pipelines across per-chunk buffers and skip redundant pipeline binds
This commit is contained in:
@@ -177,6 +177,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
entry.attributes = builder.GetAttributes();
|
entry.attributes = builder.GetAttributes();
|
||||||
|
// See the layoutHash declaration: hash only the resolved layout, never
|
||||||
|
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||||
|
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||||
|
for (const auto& binding : entry.bindings) {
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||||
|
}
|
||||||
|
for (const auto& attribute : entry.attributes) {
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||||
|
}
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||||
|
entry.layoutHash = XXH64_digest(m_hashState);
|
||||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
|
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||||
|
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
||||||
|
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
||||||
|
// pipelines on that minted one VkPipeline per chunk section for an
|
||||||
|
// identical layout, defeating pipeline reuse and the per-draw memo.
|
||||||
|
// Pipelines depend only on the layout, so they key on this instead.
|
||||||
|
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;
|
Uint64 lastUsedFrameBoundary = 0;
|
||||||
|
|||||||
@@ -226,6 +226,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// set (blit, depth-mipmap) binds - their static state makes the
|
// set (blit, depth-mipmap) binds - their static state makes the
|
||||||
// corresponding dynamic values undefined per the spec.
|
// corresponding dynamic values undefined per the spec.
|
||||||
struct DynamicStateShadow {
|
struct DynamicStateShadow {
|
||||||
|
// Last graphics pipeline bound on the frame command buffer. Pipeline
|
||||||
|
// binds are command-buffer state (they survive render-pass boundaries),
|
||||||
|
// so the same reset points that invalidate dynamic state - recording
|
||||||
|
// (re)begin and the aux blit pipelines' raw binds - are exactly the
|
||||||
|
// points where this becomes unknown.
|
||||||
|
Bool graphicsPipelineValid = false;
|
||||||
|
VkPipeline graphicsPipeline = VK_NULL_HANDLE;
|
||||||
Bool viewportValid = false;
|
Bool viewportValid = false;
|
||||||
VkViewport viewport{};
|
VkViewport viewport{};
|
||||||
Bool scissorValid = false;
|
Bool scissorValid = false;
|
||||||
@@ -3815,16 +3822,24 @@ void main() {
|
|||||||
// content hash (folds program identity + link version + transform flags + shader stages),
|
// content hash (folds program identity + link version + transform flags + shader stages),
|
||||||
// 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 m_lastPipelineResult can never dangle.
|
// Reset per-frame and on pipeline destruction so a memoized handle can never dangle.
|
||||||
const Uint64 vertexInputHash = m_vertexInputStateFactory->GetOrComputeHash(vao);
|
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);
|
||||||
|
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();
|
||||||
if (m_lastPipelineValid && m_lastPipelineResult != VK_NULL_HANDLE && m_lastPipelineMode == mode &&
|
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||||
m_lastPipelineProgramHash == programObj.hash && m_lastPipelineVertexInputHash == vertexInputHash &&
|
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
||||||
m_lastPipelineRenderPassHash == renderPassHash &&
|
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||||
m_lastPipelineRenderStateVersion == renderStateVersion &&
|
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
||||||
m_lastPipelineTransformFlags == transformFlags) {
|
entry.renderPassHash == renderPassHash &&
|
||||||
return m_lastPipelineResult;
|
entry.renderStateVersion == renderStateVersion &&
|
||||||
|
entry.transformFlags == transformFlags) {
|
||||||
|
return entry.pipeline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
@@ -3865,8 +3880,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// vertexInputHash was computed above for the fast-path key; reuse it here.
|
|
||||||
auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao, vertexInputHash);
|
|
||||||
const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes);
|
const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes);
|
||||||
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
||||||
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
||||||
@@ -3977,7 +3990,7 @@ void main() {
|
|||||||
|
|
||||||
PipelineFactory::PipelineCreatePayload payload {
|
PipelineFactory::PipelineCreatePayload payload {
|
||||||
.programHash = programObj.hash,
|
.programHash = programObj.hash,
|
||||||
.vertexInputHash = vertexInputHash,
|
.vertexInputHash = vertexLayoutHash,
|
||||||
.pipelineLayout = programObj.pipelineLayout,
|
.pipelineLayout = programObj.pipelineLayout,
|
||||||
.renderPass = renderPassEntry.renderPass,
|
.renderPass = renderPassEntry.renderPass,
|
||||||
.colorAttachmentCount = renderPassEntry.colorAttachmentCount,
|
.colorAttachmentCount = renderPassEntry.colorAttachmentCount,
|
||||||
@@ -4284,14 +4297,16 @@ void main() {
|
|||||||
}
|
}
|
||||||
VkPipeline pipeline = m_pipelineFactory->GetOrCreatePipeline(payload);
|
VkPipeline pipeline = m_pipelineFactory->GetOrCreatePipeline(payload);
|
||||||
if (pipeline != VK_NULL_HANDLE) {
|
if (pipeline != VK_NULL_HANDLE) {
|
||||||
m_lastPipelineValid = true;
|
PipelineMemoEntry& entry = m_pipelineMemo[m_pipelineMemoNext];
|
||||||
m_lastPipelineMode = mode;
|
entry.mode = mode;
|
||||||
m_lastPipelineProgramHash = programObj.hash;
|
entry.programHash = programObj.hash;
|
||||||
m_lastPipelineVertexInputHash = vertexInputHash;
|
entry.vertexInputHash = vertexLayoutHash;
|
||||||
m_lastPipelineRenderPassHash = renderPassHash;
|
entry.renderPassHash = renderPassHash;
|
||||||
m_lastPipelineRenderStateVersion = renderStateVersion;
|
entry.renderStateVersion = renderStateVersion;
|
||||||
m_lastPipelineTransformFlags = transformFlags;
|
entry.transformFlags = transformFlags;
|
||||||
m_lastPipelineResult = pipeline;
|
entry.pipeline = pipeline;
|
||||||
|
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
|
||||||
|
m_pipelineMemoCount = std::min(m_pipelineMemoCount + 1, kPipelineMemoSize);
|
||||||
}
|
}
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
@@ -4629,7 +4644,11 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
|
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!g_dynamicStateShadow.graphicsPipelineValid || g_dynamicStateShadow.graphicsPipeline != pipeline) {
|
||||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||||
|
g_dynamicStateShadow.graphicsPipelineValid = true;
|
||||||
|
g_dynamicStateShadow.graphicsPipeline = pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
|
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
|
||||||
@@ -6412,7 +6431,7 @@ void main() {
|
|||||||
if (frame.isCommandRecording) {
|
if (frame.isCommandRecording) {
|
||||||
m_frameContext.EndCommandRecording();
|
m_frameContext.EndCommandRecording();
|
||||||
frame.hasCommandBufferRecorded = true;
|
frame.hasCommandBufferRecorded = true;
|
||||||
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
|
InvalidatePipelineMemo(); // command-buffer boundary: drop the pipeline memo
|
||||||
}
|
}
|
||||||
// The pre-pass stream must never be submitted later than the recording
|
// The pre-pass stream must never be submitted later than the recording
|
||||||
// it was paired with (frame commands recorded after a pre-pass move
|
// it was paired with (frame commands recorded after a pre-pass move
|
||||||
@@ -7499,8 +7518,7 @@ void main() {
|
|||||||
m_programFactory->OnFrameBoundary();
|
m_programFactory->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
|
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||||
m_lastPipelineValid = false;
|
InvalidatePipelineMemo();
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
if (m_vertexInputStateFactory) {
|
if (m_vertexInputStateFactory) {
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
@@ -7617,8 +7635,7 @@ void main() {
|
|||||||
// cache and the aging sweep could destroy it while the flushed submission
|
// cache and the aging sweep could destroy it while the flushed submission
|
||||||
// still references it. Mirrors the drops at the readback and Present
|
// still references it. Mirrors the drops at the readback and Present
|
||||||
// boundaries; costs one full pipeline lookup on the next draw.
|
// boundaries; costs one full pipeline lookup on the next draw.
|
||||||
m_lastPipelineValid = false;
|
InvalidatePipelineMemo();
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
|
|
||||||
// The submitted command buffer may still be executing; recording must
|
// The submitted command buffer may still be executing; recording must
|
||||||
// restart on a fresh one. If none can be allocated, fall back to
|
// restart on a fresh one. If none can be allocated, fall back to
|
||||||
@@ -7771,7 +7788,7 @@ void main() {
|
|||||||
m_frameContext.AbandonPreCommandRecording();
|
m_frameContext.AbandonPreCommandRecording();
|
||||||
suspendedFrame.isCommandRecording = false;
|
suspendedFrame.isCommandRecording = false;
|
||||||
suspendedFrame.hasCommandBufferRecorded = false;
|
suspendedFrame.hasCommandBufferRecorded = false;
|
||||||
m_lastPipelineValid = false;
|
InvalidatePipelineMemo();
|
||||||
// The dropped recording is never submitted, so once the fence
|
// The dropped recording is never submitted, so once the fence
|
||||||
// poll shows the pre-suspension submissions complete the frame
|
// poll shows the pre-suspension submissions complete the frame
|
||||||
// transients (descriptor sets, transient arenas, deferred
|
// transients (descriptor sets, transient arenas, deferred
|
||||||
@@ -7807,8 +7824,10 @@ void main() {
|
|||||||
// performs a real, stamping lookup) and can never age out.
|
// performs a real, stamping lookup) and can never age out.
|
||||||
m_programFactory->OnFrameBoundary();
|
m_programFactory->OnFrameBoundary();
|
||||||
if (m_pipelineFactory->OnFrameBoundary() > 0) {
|
if (m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||||
m_lastPipelineValid = false; // an aged-out pipeline may still be memoized
|
InvalidatePipelineMemo(); // an aged-out pipeline may still be memoized
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
// A recreated pipeline could reuse a freed handle value and alias
|
||||||
|
// the bind-dedup shadow; force the next draw to re-bind.
|
||||||
|
g_dynamicStateShadow.graphicsPipelineValid = false;
|
||||||
}
|
}
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
m_samplerManager->OnFrameBoundary();
|
m_samplerManager->OnFrameBoundary();
|
||||||
@@ -7831,7 +7850,7 @@ void main() {
|
|||||||
if (frame.isCommandRecording) {
|
if (frame.isCommandRecording) {
|
||||||
m_frameContext.EndCommandRecording();
|
m_frameContext.EndCommandRecording();
|
||||||
frame.hasCommandBufferRecorded = true;
|
frame.hasCommandBufferRecorded = true;
|
||||||
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
|
InvalidatePipelineMemo(); // command-buffer boundary: drop the pipeline memo
|
||||||
}
|
}
|
||||||
m_frameContext.EndPreCommandRecordingIfOpen();
|
m_frameContext.EndPreCommandRecordingIfOpen();
|
||||||
|
|
||||||
@@ -8834,7 +8853,8 @@ void main() {
|
|||||||
if (m_pipelineFactory) {
|
if (m_pipelineFactory) {
|
||||||
m_pipelineFactory->DestroyAll();
|
m_pipelineFactory->DestroyAll();
|
||||||
}
|
}
|
||||||
m_lastPipelineValid = false; // pipelines freed -> the memoized handle would dangle
|
InvalidatePipelineMemo(); // pipelines freed -> the memoized handle would dangle
|
||||||
|
g_dynamicStateShadow.graphicsPipelineValid = false;
|
||||||
DestroyComputePipelines();
|
DestroyComputePipelines();
|
||||||
if (m_frameContext.GetFrameCount() > 0) {
|
if (m_frameContext.GetFrameCount() > 0) {
|
||||||
m_frameContext.GetCurrent().isCommandRecording = false;
|
m_frameContext.GetCurrent().isCommandRecording = false;
|
||||||
@@ -8997,8 +9017,7 @@ void main() {
|
|||||||
// destroys them immediately. The memo must drop as well: it can hand out a
|
// destroys them immediately. The memo must drop as well: it can hand out a
|
||||||
// cached handle without touching the factory.
|
// cached handle without touching the factory.
|
||||||
if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) {
|
if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) {
|
||||||
m_lastPipelineValid = false;
|
InvalidatePipelineMemo();
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9017,8 +9036,7 @@ void main() {
|
|||||||
m_computePipelines.erase(computeIt);
|
m_computePipelines.erase(computeIt);
|
||||||
}
|
}
|
||||||
if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) {
|
if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) {
|
||||||
m_lastPipelineValid = false;
|
InvalidatePipelineMemo();
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
if (m_uniformManager != nullptr) {
|
if (m_uniformManager != nullptr) {
|
||||||
m_uniformManager->OnDescriptorSetLayoutDestroyed(descriptorSetLayout);
|
m_uniformManager->OnDescriptorSetLayoutDestroyed(descriptorSetLayout);
|
||||||
|
|||||||
@@ -455,14 +455,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
||||||
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
||||||
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
||||||
Bool m_lastPipelineValid = false;
|
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||||
GLenum m_lastPipelineMode = 0;
|
// single-entry memo thrashed on draw sequences that alternate a few
|
||||||
Uint64 m_lastPipelineProgramHash = 0;
|
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
// payload-hash lookup per draw; eight entries cover such working sets
|
||||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
// while keeping the hit path a trivial linear scan.
|
||||||
Uint m_lastPipelineRenderStateVersion = 0;
|
struct PipelineMemoEntry {
|
||||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
GLenum mode = 0;
|
||||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
Uint64 programHash = 0;
|
||||||
|
Uint64 vertexInputHash = 0;
|
||||||
|
Uint64 renderPassHash = 0;
|
||||||
|
Uint renderStateVersion = 0;
|
||||||
|
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||||
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
};
|
||||||
|
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||||
|
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||||
|
Uint32 m_pipelineMemoCount = 0;
|
||||||
|
Uint32 m_pipelineMemoNext = 0;
|
||||||
|
// Drops every memoized pipeline handle. Required at command-buffer
|
||||||
|
// boundaries and whenever any pipeline may have been destroyed.
|
||||||
|
void InvalidatePipelineMemo() {
|
||||||
|
m_pipelineMemoCount = 0;
|
||||||
|
m_pipelineMemoNext = 0;
|
||||||
|
}
|
||||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||||
UniquePtr<ProgramFactory> m_programFactory;
|
UniquePtr<ProgramFactory> m_programFactory;
|
||||||
UniquePtr<UniformManager> m_uniformManager;
|
UniquePtr<UniformManager> m_uniformManager;
|
||||||
|
|||||||
Reference in New Issue
Block a user