[Perf] (MG_Backend): let DirectVulkan's draw fast path survive a VAO swap

TrySetupDrawFastPath declined on its VAO pointer check for every draw of
a 512-VAO cycle - the Blaze3D chunk-render shape - so the fast path was
dead exactly where it mattered: full SetupDraw, per-draw
ResolveSamplerDescriptor, SyncTextureAndGetDescriptor and render-pass
re-fetch, for draws whose only change was the VAO.

Three fixes. A moved VAO now re-runs only the vertex-input pre-flight
and re-resolves the pipeline instead of declining to the full path. That
resolution probes the value-keyed pipeline memo directly off a cached
pipeline-state hash and snapshot render-pass hash, skipping
GetOrCreateRenderPass and its GetPendingRenderbufferClear probes per
draw; a stale cached hash can only miss, never false-hit. And when the
sampler-descriptor hint holds and the program's single dynamic UBO
re-resolves to the same VkBuffer and range - only the dynamic offset
moved, the per-draw glUniform case - the descriptor walk collapses to
one offset recompute and a vkCmdBindDescriptorSets of the same recorded
set with new pDynamicOffsets. The rebind memo is invalidated at
BeginFrame, layout destruction and override walks; the program lifetime
id never repeats, and per-frame descriptor sets are never rewritten
within their frame.

mc_vanilla_draw -36.9% (1260 -> 795 ns/op, 4.6x native to 3.4x),
sodium_multidraw -18.0%, state_toggle -14.9%, sampler_churn -7.8%,
use_program -7.7%, ubo_range -7.3%, tex_param -7.3%. All nine cases on
both backends, interleaved A/B; no attributable regression. Unit tests
421/421.
This commit is contained in:
BZLZHH
2026-08-06 12:00:31 -04:00
parent b904658b10
commit d0aae85da2
3 changed files with 275 additions and 102 deletions
@@ -207,6 +207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
@@ -248,6 +249,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
// The rebind memo's set may be among the freed ones.
m_fastRebindMemo.valid = false;
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
@@ -1242,6 +1245,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_SUCCESS;
}
Bool UniformManager::ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 arrayElement, Uint32 frameIndex,
VkBuffer& outBuffer, VkDeviceSize& outRange,
Uint32& outDynamicOffset) {
UboBindResult ubo{};
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, arrayElement, ubo);
MOBILEGL_ASSERT(hasPayload && (ubo.directBindable || (ubo.payload != nullptr && ubo.payloadSize > 0)),
"UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: missing UBO payload on binding %u element %u",
binding, arrayElement);
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
outBuffer = ubo.buffer;
outRange = ubo.range;
outDynamicOffset = static_cast<Uint32>(ubo.dynamicOffset);
return true;
}
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
// uniform bytes re-use the slice already uploaded this frame.
const Bool isGlobalUbo = programObj.globalUboBinding == static_cast<Int>(binding) && arrayElement == 0;
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
const Uint32 uboContentVersion = program.GetUBOContentVersion();
if (isGlobalUbo) {
for (const auto& memo : m_globalUboMemo) {
if (memo.buffer != VK_NULL_HANDLE && memo.programLifetimeId == uboProgramLifetimeId &&
memo.frameSerial == uboFrameSerial && memo.uboContentVersion == uboContentVersion &&
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
outBuffer = memo.buffer;
outRange = memo.range;
outDynamicOffset = static_cast<Uint32>(memo.offset);
return true;
}
}
}
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, ubo.payloadSize,
m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false,
"UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: UBO upload failed on binding %u element %u",
binding, arrayElement);
return false;
}
outBuffer = slice.buffer;
outRange = ubo.payloadSize;
outDynamicOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] =
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
}
return true;
}
void UniformManager::BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets) {
// Skip the driver call when this exact binding is already live on the
// command buffer (see the bind-dedup shadow in the header).
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
m_lastBindLayout == pipelineLayout && m_lastBindPoint == bindPoint &&
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
if (identicalBind) {
for (Uint32 i = 0; i < offsetCount; ++i) {
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
identicalBind = false;
break;
}
}
}
if (!identicalBind) {
vkCmdBindDescriptorSets(commandBuffer, bindPoint, pipelineLayout, 0, 1,
&descriptorSet, offsetCount, dynamicOffsets.data());
if (offsetCount <= kMaxShadowedDynamicOffsets) {
m_lastBindValid = true;
m_lastBindSet = descriptorSet;
m_lastBindLayout = pipelineLayout;
m_lastBindPoint = bindPoint;
m_lastBindOffsetCount = offsetCount;
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
} else {
m_lastBindValid = false;
}
}
}
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
@@ -1258,6 +1349,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
}
// Dynamic-offset-only rebind (see FastRebindMemo in the header): the last
// cacheable walk of this exact program selected a set whose contents are
// provably still what this walk would write - the hint covers every
// sampler binding, and an unchanged (buffer, range) for the single
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
// the SAME set delivers without any descriptor write.
const Bool cacheable = (samplerBindingOverride == nullptr);
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
m_fastRebindMemo.frameIndex == frameIndex &&
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
m_fastRebindMemo.programHash == programObj.hash) {
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
Uint32 uboDynamicOffset = 0;
if (ResolveDynamicUboDescriptor(program, programObj, m_fastRebindMemo.uboBinding, 0, frameIndex,
uboBuffer, uboRange, uboDynamicOffset) &&
uboBuffer == m_fastRebindMemo.uboBuffer && uboRange == m_fastRebindMemo.uboRange) {
auto& fastOffsets = m_dynamicOffsetsScratch;
fastOffsets.clear();
fastOffsets.push_back(uboDynamicOffset);
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout,
m_fastRebindMemo.set, fastOffsets);
return true;
}
// Any mismatch (arena wrap or growth, direct-bind retarget, upload
// failure) falls through to the full walk, which re-records the memo.
}
// The descriptor set is chosen AFTER the writes are built (below), so a draw
// whose resolved descriptor content matches the previous draw can reuse that
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
@@ -1289,6 +1408,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
// Eligibility probe for FastRebindMemo, filled by this walk: exactly one
// dynamic-UBO descriptor (no arrayed elements) and otherwise only
// combined-image samplers, so the whole set's content is pinned by the
// sampler hint plus one (buffer, range) compare.
Uint32 dynamicUboDescriptorCount = 0;
Uint32 fastRebindUboBinding = 0;
Bool fastRebindKindsEligible = true;
// Iterate only the bindings this program declares. The old walk covered all 256 slots of
// bindingKinds on every draw to find the 1-8 a real program uses.
for (const Uint32 binding : programObj.activeBindings) {
@@ -1309,67 +1436,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
dynamicUboDescriptorCount += descriptorCount;
fastRebindUboBinding = binding;
const SizeT firstBufferInfoIndex = bufferInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
UboBindResult ubo{};
const Bool hasPayload =
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
binding, element);
VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0;
Uint32 dynOffset;
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
// uniform bytes re-use the slice already uploaded this frame.
const Bool isGlobalUbo =
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
const Uint32 uboContentVersion = program.GetUBOContentVersion();
Bool reusedSlice = false;
if (isGlobalUbo) {
for (const auto& memo : m_globalUboMemo) {
if (memo.buffer != VK_NULL_HANDLE &&
memo.programLifetimeId == uboProgramLifetimeId &&
memo.frameSerial == uboFrameSerial &&
memo.uboContentVersion == uboContentVersion &&
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
bufferInfo.buffer = memo.buffer;
bufferInfo.range = memo.range;
dynOffset = static_cast<Uint32>(memo.offset);
reusedSlice = true;
break;
}
}
}
if (!reusedSlice) {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
}
}
Uint32 dynOffset = 0;
if (!ResolveDynamicUboDescriptor(program, programObj, binding, element, frameIndex,
bufferInfo.buffer, bufferInfo.range, dynOffset)) {
return false;
}
bufferInfos.push_back(bufferInfo);
// Dynamic offsets are consumed in binding order, then array element order,
@@ -1392,6 +1470,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
texelBufferViews.push_back(bufferView);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
@@ -1405,6 +1484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
bufferInfos.push_back(bufferInfo);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write.pBufferInfo = &bufferInfos.back();
writes.push_back(write);
@@ -1417,6 +1497,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
imageInfos.push_back(imageInfo);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
write.pImageInfo = &imageInfos.back();
writes.push_back(write);
@@ -1461,7 +1542,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// cursor only advances), so its written contents survive; the layout is part of
// the signature so reuse never crosses programs. Sampler overrides (blits)
// bypass and invalidate the cache.
const Bool cacheable = (samplerBindingOverride == nullptr);
Uint64 signature = 0xcbf29ce484222325ULL;
{
const auto mix64 = [&signature](Uint64 word) {
@@ -1524,34 +1604,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Skip the driver call when this exact binding is already live on the
// command buffer (see the bind-dedup shadow in the header).
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
if (identicalBind) {
for (Uint32 i = 0; i < offsetCount; ++i) {
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
identicalBind = false;
break;
}
}
}
if (!identicalBind) {
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, offsetCount, dynamicOffsets.data());
if (offsetCount <= kMaxShadowedDynamicOffsets) {
m_lastBindValid = true;
m_lastBindSet = descriptorSet;
m_lastBindLayout = programObj.pipelineLayout;
m_lastBindPoint = bindPoint;
m_lastBindOffsetCount = offsetCount;
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
} else {
m_lastBindValid = false;
}
// (Re)record the dynamic-offset-only rebind memo. Recording on every
// cacheable walk (allocated or reused set alike - both hold exactly the
// content just computed) keeps the single slot tracking the most recent
// program; a non-cacheable override walk drops it alongside the reuse
// memo above.
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
m_fastRebindMemo = FastRebindMemo{
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
fastRebindUboBinding, bufferInfos[0].buffer,
bufferInfos[0].range, descriptorSet};
} else {
m_fastRebindMemo.valid = false;
}
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet,
dynamicOffsets);
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -186,6 +186,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const;
// Shared resolution of one dynamic-UBO binding element into the
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
// bind, global-slice reuse, or transient upload. Used by the full walk
// and by the dynamic-offset-only rebind (see FastRebindMemo).
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
VkDeviceSize& outRange, Uint32& outDynamicOffset);
// The vkCmdBindDescriptorSets tail shared by the full walk and the
// dynamic-offset-only rebind: skips the driver call when this exact
// binding is already live on the command buffer (see the bind-dedup
// shadow below), otherwise binds and refreshes the shadow.
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool(
@@ -233,6 +248,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
// descriptor set selected by the last cacheable full walk of a program
// whose active bindings are exactly one dynamic UBO (single descriptor)
// plus combined-image samplers. When the next call proves every sampler
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
// moved, the per-draw glUniform case - the walk collapses to: resolve one
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
// rebinding the same set with different dynamic offsets).
// Invalidation inventory: BeginFrame clears it (the frame's sets are
// recycled) and the frameIndex field guards cross-frame confusion on top;
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
// program relink bumps the backend state version and thus programObj.hash
// so the key misses; the program lifetime id is never reused, so a
// deleted-and-recreated program misses; a texture/sampler/binding change
// drops the hint upstream; an arena wrap or growth resolves a different
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
// advances, so the recorded set is never re-written within its frame.
struct FastRebindMemo {
Bool valid = false;
Uint32 frameIndex = 0;
Uint64 programLifetimeId = 0;
ProgramFactory::HashType programHash = 0;
Uint32 uboBinding = 0;
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
};
FastRebindMemo m_fastRebindMemo;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
@@ -4916,10 +4916,15 @@ void main() {
program.GetBackendStateVersion() != snap.programVersion) {
return false;
}
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
// input state (re-resolved below through the layout-keyed memo, so N VAOs
// sharing one attribute layout share one pipeline) and the vertex/index
// buffer binds (re-run every draw anyway). Declining here would send every
// draw of a VAO-cycling stream (Minecraft chunk rendering) through the full
// path, re-resolving descriptors and texture layouts nothing invalidated.
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
if (static_cast<const void*>(&vao) != snap.vao || vao.GetConfigVersion() != snap.vaoConfigVersion) {
return false;
}
const Bool vaoMoved =
static_cast<const void*>(&vao) != snap.vao || vao.GetConfigVersion() != snap.vaoConfigVersion;
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
@@ -4956,6 +4961,30 @@ void main() {
}
const auto& programObj = m_programFactory->GetOrCreateProgram(
program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags));
if (vaoMoved) {
// Vertex-input pre-flight for the changed VAO, mirroring the full path:
// a bad attribute must never be baked into a cached VkPipeline, and the
// current-value synthesis in UploadAndBindVertexBuffers must never see
// an unsupported generic-attribute type. Declining routes the draw
// through the full path's loud failure reporting.
const auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
if ((vertexInputState.unsupportedAttribMask & activeAttribMask) != 0) {
return false;
}
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputState.attributeLocationMask;
if (missingAttribMask != 0) {
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
if ((missingAttribMask & (1u << location)) == 0) {
continue;
}
if (MG_State::GLState::ClassifyVertexAttribType(programObj.vertexInputTypes[location])
.baseType == MG_State::GLState::VertexAttribBaseType::Unsupported) {
return false;
}
}
}
}
if (bindsMoved &&
!m_uniformManager->SampledBindingsUnchanged(program, programObj, m_sampledBindingRecordsScratch)) {
return false;
@@ -5014,31 +5043,61 @@ void main() {
}
// Everything the full path would re-resolve is provably unchanged - or, for
// a moved pipeline-state version, reduces to re-resolving just the pipeline
// through the value-keyed memo against the still-active render pass. Run
// only the per-draw tail.
// a moved pipeline-state version or a changed VAO, reduces to re-resolving
// just the pipeline through the value-keyed memo against the still-active
// render pass. Run only the per-draw tail.
VkPipeline pipeline = snap.pipeline;
if (renderStateMoved) {
// Same lookup the full path would do; every input (FBO + version, image
// index, depth/stencil participation, image epochs, no pending clears)
// was verified unchanged above, so this is a pure cache hit on the same
// entry the snapshot's pipeline was built against.
const RenderPassEntry& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
*drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil);
if (!activeRenderPass->CompatibleWith(renderPassEntry)) {
return false;
if (renderStateMoved || vaoMoved) {
pipeline = VK_NULL_HANDLE;
if (!renderStateMoved && m_pipelineStateHashValid &&
m_pipelineStateHashVersion == renderStateVersion) {
// VAO-only movement: the render pass is provably the snapshot's (hash
// match above) and the pipeline-state hash is cached for this
// untouched state version, so probe the value-keyed pipeline memo
// directly - no render-pass-entry re-fetch (whose pending-clear
// probes cost more than the whole probe below). A miss, or a cached
// hash computed against another pass's attachment count (the hash
// folds it in, so such a mismatch can only produce a miss, never a
// false hit), falls through to the full lookup.
const auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
const auto memoTransformFlags =
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags);
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 == vis.layoutHash &&
entry.renderPassHash == snap.renderPassHash &&
entry.pipelineStateHash == m_pipelineStateHash &&
entry.transformFlags == memoTransformFlags) {
pipeline = entry.pipeline;
break;
}
}
}
pipeline = GetOrCreatePipeline(mode, program, programObj,
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
vao, renderPassEntry);
if (pipeline == VK_NULL_HANDLE) {
return false;
// Same lookup the full path would do; every input (FBO + version, image
// index, depth/stencil participation, image epochs, no pending clears)
// was verified unchanged above, so this is a pure cache hit on the same
// entry the snapshot's pipeline was built against.
const RenderPassEntry& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
*drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil);
if (!activeRenderPass->CompatibleWith(renderPassEntry)) {
return false;
}
pipeline = GetOrCreatePipeline(mode, program, programObj,
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
vao, renderPassEntry);
if (pipeline == VK_NULL_HANDLE) {
return false;
}
}
}
// Every decline is behind us: the snapshot again describes the current
// counters, so the next draw's compare is two integer loads.
snap.renderStateVersion = renderStateVersion;
snap.bindGeneration = bindGeneration;
snap.vao = static_cast<const void*>(&vao);
snap.vaoConfigVersion = vao.GetConfigVersion();
snap.pipeline = pipeline;
if (!g_dynamicStateShadow.graphicsPipelineValid ||
g_dynamicStateShadow.graphicsPipeline != pipeline) {