mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +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.bindings = builder.GetBindings();
|
||||
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.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
struct BackendVertexInputState {
|
||||
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
|
||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
|
||||
@@ -226,6 +226,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// set (blit, depth-mipmap) binds - their static state makes the
|
||||
// corresponding dynamic values undefined per the spec.
|
||||
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;
|
||||
VkViewport viewport{};
|
||||
Bool scissorValid = false;
|
||||
@@ -3815,16 +3822,24 @@ void main() {
|
||||
// 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
|
||||
// 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);
|
||||
// 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 Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
if (m_lastPipelineValid && m_lastPipelineResult != VK_NULL_HANDLE && m_lastPipelineMode == mode &&
|
||||
m_lastPipelineProgramHash == programObj.hash && m_lastPipelineVertexInputHash == vertexInputHash &&
|
||||
m_lastPipelineRenderPassHash == renderPassHash &&
|
||||
m_lastPipelineRenderStateVersion == renderStateVersion &&
|
||||
m_lastPipelineTransformFlags == transformFlags) {
|
||||
return m_lastPipelineResult;
|
||||
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
||||
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
||||
entry.renderPassHash == renderPassHash &&
|
||||
entry.renderStateVersion == renderStateVersion &&
|
||||
entry.transformFlags == transformFlags) {
|
||||
return entry.pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
@@ -3865,8 +3880,6 @@ void main() {
|
||||
}
|
||||
#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 activeAttribMask = programObj.activeVertexInputLocationMask;
|
||||
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
||||
@@ -3977,7 +3990,7 @@ void main() {
|
||||
|
||||
PipelineFactory::PipelineCreatePayload payload {
|
||||
.programHash = programObj.hash,
|
||||
.vertexInputHash = vertexInputHash,
|
||||
.vertexInputHash = vertexLayoutHash,
|
||||
.pipelineLayout = programObj.pipelineLayout,
|
||||
.renderPass = renderPassEntry.renderPass,
|
||||
.colorAttachmentCount = renderPassEntry.colorAttachmentCount,
|
||||
@@ -4284,14 +4297,16 @@ void main() {
|
||||
}
|
||||
VkPipeline pipeline = m_pipelineFactory->GetOrCreatePipeline(payload);
|
||||
if (pipeline != VK_NULL_HANDLE) {
|
||||
m_lastPipelineValid = true;
|
||||
m_lastPipelineMode = mode;
|
||||
m_lastPipelineProgramHash = programObj.hash;
|
||||
m_lastPipelineVertexInputHash = vertexInputHash;
|
||||
m_lastPipelineRenderPassHash = renderPassHash;
|
||||
m_lastPipelineRenderStateVersion = renderStateVersion;
|
||||
m_lastPipelineTransformFlags = transformFlags;
|
||||
m_lastPipelineResult = pipeline;
|
||||
PipelineMemoEntry& entry = m_pipelineMemo[m_pipelineMemoNext];
|
||||
entry.mode = mode;
|
||||
entry.programHash = programObj.hash;
|
||||
entry.vertexInputHash = vertexLayoutHash;
|
||||
entry.renderPassHash = renderPassHash;
|
||||
entry.renderStateVersion = renderStateVersion;
|
||||
entry.transformFlags = transformFlags;
|
||||
entry.pipeline = pipeline;
|
||||
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
|
||||
m_pipelineMemoCount = std::min(m_pipelineMemoCount + 1, kPipelineMemoSize);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
@@ -4629,7 +4644,11 @@ void main() {
|
||||
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
|
||||
}
|
||||
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
if (!g_dynamicStateShadow.graphicsPipelineValid || g_dynamicStateShadow.graphicsPipeline != pipeline) {
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
g_dynamicStateShadow.graphicsPipelineValid = true;
|
||||
g_dynamicStateShadow.graphicsPipeline = pipeline;
|
||||
}
|
||||
|
||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
|
||||
@@ -6412,7 +6431,7 @@ void main() {
|
||||
if (frame.isCommandRecording) {
|
||||
m_frameContext.EndCommandRecording();
|
||||
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
|
||||
// it was paired with (frame commands recorded after a pre-pass move
|
||||
@@ -7499,8 +7518,7 @@ void main() {
|
||||
m_programFactory->OnFrameBoundary();
|
||||
}
|
||||
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
InvalidatePipelineMemo();
|
||||
}
|
||||
if (m_vertexInputStateFactory) {
|
||||
m_vertexInputStateFactory->OnFrameBoundary();
|
||||
@@ -7617,8 +7635,7 @@ void main() {
|
||||
// cache and the aging sweep could destroy it while the flushed submission
|
||||
// still references it. Mirrors the drops at the readback and Present
|
||||
// boundaries; costs one full pipeline lookup on the next draw.
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
InvalidatePipelineMemo();
|
||||
|
||||
// The submitted command buffer may still be executing; recording must
|
||||
// restart on a fresh one. If none can be allocated, fall back to
|
||||
@@ -7771,7 +7788,7 @@ void main() {
|
||||
m_frameContext.AbandonPreCommandRecording();
|
||||
suspendedFrame.isCommandRecording = false;
|
||||
suspendedFrame.hasCommandBufferRecorded = false;
|
||||
m_lastPipelineValid = false;
|
||||
InvalidatePipelineMemo();
|
||||
// The dropped recording is never submitted, so once the fence
|
||||
// poll shows the pre-suspension submissions complete the frame
|
||||
// transients (descriptor sets, transient arenas, deferred
|
||||
@@ -7807,8 +7824,10 @@ void main() {
|
||||
// performs a real, stamping lookup) and can never age out.
|
||||
m_programFactory->OnFrameBoundary();
|
||||
if (m_pipelineFactory->OnFrameBoundary() > 0) {
|
||||
m_lastPipelineValid = false; // an aged-out pipeline may still be memoized
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
InvalidatePipelineMemo(); // an aged-out pipeline may still be memoized
|
||||
// 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_samplerManager->OnFrameBoundary();
|
||||
@@ -7831,7 +7850,7 @@ void main() {
|
||||
if (frame.isCommandRecording) {
|
||||
m_frameContext.EndCommandRecording();
|
||||
frame.hasCommandBufferRecorded = true;
|
||||
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
|
||||
InvalidatePipelineMemo(); // command-buffer boundary: drop the pipeline memo
|
||||
}
|
||||
m_frameContext.EndPreCommandRecordingIfOpen();
|
||||
|
||||
@@ -8834,7 +8853,8 @@ void main() {
|
||||
if (m_pipelineFactory) {
|
||||
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();
|
||||
if (m_frameContext.GetFrameCount() > 0) {
|
||||
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
|
||||
// cached handle without touching the factory.
|
||||
if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) {
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
InvalidatePipelineMemo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9017,8 +9036,7 @@ void main() {
|
||||
m_computePipelines.erase(computeIt);
|
||||
}
|
||||
if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) {
|
||||
m_lastPipelineValid = false;
|
||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
InvalidatePipelineMemo();
|
||||
}
|
||||
if (m_uniformManager != nullptr) {
|
||||
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
|
||||
// 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.
|
||||
Bool m_lastPipelineValid = false;
|
||||
GLenum m_lastPipelineMode = 0;
|
||||
Uint64 m_lastPipelineProgramHash = 0;
|
||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
||||
Uint m_lastPipelineRenderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||
// single-entry memo thrashed on draw sequences that alternate a few
|
||||
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||
// payload-hash lookup per draw; eight entries cover such working sets
|
||||
// while keeping the hit path a trivial linear scan.
|
||||
struct PipelineMemoEntry {
|
||||
GLenum mode = 0;
|
||||
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;
|
||||
UniquePtr<ProgramFactory> m_programFactory;
|
||||
UniquePtr<UniformManager> m_uniformManager;
|
||||
|
||||
Reference in New Issue
Block a user