mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user