mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +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) {
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
entry.valid = false;
|
entry.valid = false;
|
||||||
}
|
}
|
||||||
|
m_fastRebindMemo.valid = false;
|
||||||
m_lastBindValid = false;
|
m_lastBindValid = false;
|
||||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||||
@@ -248,6 +249,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& entry : m_descriptorReuseMemo) {
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
entry.valid = false;
|
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);
|
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1242,6 +1245,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return VK_SUCCESS;
|
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,
|
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -1258,6 +1349,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frame.activeDescriptorPoolIndex = 0;
|
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
|
// 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
|
// whose resolved descriptor content matches the previous draw can reuse that
|
||||||
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
|
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
|
||||||
@@ -1289,6 +1408,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
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
|
// 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.
|
// bindingKinds on every draw to find the 1-8 a real program uses.
|
||||||
for (const Uint32 binding : programObj.activeBindings) {
|
for (const Uint32 binding : programObj.activeBindings) {
|
||||||
@@ -1309,68 +1436,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
binding < programObj.bindingDescriptorCounts.size()
|
binding < programObj.bindingDescriptorCounts.size()
|
||||||
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
||||||
: 1u;
|
: 1u;
|
||||||
|
dynamicUboDescriptorCount += descriptorCount;
|
||||||
|
fastRebindUboBinding = binding;
|
||||||
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
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{};
|
VkDescriptorBufferInfo bufferInfo{};
|
||||||
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed 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.
|
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
||||||
bufferInfo.offset = 0;
|
bufferInfo.offset = 0;
|
||||||
Uint32 dynOffset;
|
Uint32 dynOffset = 0;
|
||||||
if (ubo.directBindable) {
|
if (!ResolveDynamicUboDescriptor(program, programObj, binding, element, frameIndex,
|
||||||
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
bufferInfo.buffer, bufferInfo.range, dynOffset)) {
|
||||||
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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bufferInfos.push_back(bufferInfo);
|
bufferInfos.push_back(bufferInfo);
|
||||||
// Dynamic offsets are consumed in binding order, then array element order,
|
// Dynamic offsets are consumed in binding order, then array element order,
|
||||||
// matching Vulkan's dynamic-offset consumption rules.
|
// matching Vulkan's dynamic-offset consumption rules.
|
||||||
@@ -1392,6 +1470,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
texelBufferViews.push_back(bufferView);
|
texelBufferViews.push_back(bufferView);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||||
write.pTexelBufferView = &texelBufferViews.back();
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
@@ -1405,6 +1484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bufferInfos.push_back(bufferInfo);
|
bufferInfos.push_back(bufferInfo);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
write.pBufferInfo = &bufferInfos.back();
|
write.pBufferInfo = &bufferInfos.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
@@ -1417,6 +1497,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
imageInfos.push_back(imageInfo);
|
imageInfos.push_back(imageInfo);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||||
write.pImageInfo = &imageInfos.back();
|
write.pImageInfo = &imageInfos.back();
|
||||||
writes.push_back(write);
|
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
|
// cursor only advances), so its written contents survive; the layout is part of
|
||||||
// the signature so reuse never crosses programs. Sampler overrides (blits)
|
// the signature so reuse never crosses programs. Sampler overrides (blits)
|
||||||
// bypass and invalidate the cache.
|
// bypass and invalidate the cache.
|
||||||
const Bool cacheable = (samplerBindingOverride == nullptr);
|
|
||||||
Uint64 signature = 0xcbf29ce484222325ULL;
|
Uint64 signature = 0xcbf29ce484222325ULL;
|
||||||
{
|
{
|
||||||
const auto mix64 = [&signature](Uint64 word) {
|
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
|
// (Re)record the dynamic-offset-only rebind memo. Recording on every
|
||||||
// command buffer (see the bind-dedup shadow in the header).
|
// cacheable walk (allocated or reused set alike - both hold exactly the
|
||||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
// content just computed) keeps the single slot tracking the most recent
|
||||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
// program; a non-cacheable override walk drops it alongside the reuse
|
||||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
// memo above.
|
||||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
|
||||||
if (identicalBind) {
|
m_fastRebindMemo = FastRebindMemo{
|
||||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
|
||||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
fastRebindUboBinding, bufferInfos[0].buffer,
|
||||||
identicalBind = false;
|
bufferInfos[0].range, descriptorSet};
|
||||||
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 {
|
} else {
|
||||||
m_lastBindValid = false;
|
m_fastRebindMemo.valid = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet,
|
||||||
|
dynamicOffsets);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -186,6 +186,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 arrayElement, UboBindResult& out) const;
|
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 CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||||
VkResult AllocateDescriptorSetsFromActivePool(
|
VkResult AllocateDescriptorSetsFromActivePool(
|
||||||
@@ -233,6 +248,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||||
Uint32 m_descriptorReuseMemoNext = 0;
|
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
|
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||||
// block resolve to the same set AND the same dynamic offsets, so the
|
// block resolve to the same set AND the same dynamic offsets, so the
|
||||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||||
|
|||||||
@@ -4916,10 +4916,15 @@ void main() {
|
|||||||
program.GetBackendStateVersion() != snap.programVersion) {
|
program.GetBackendStateVersion() != snap.programVersion) {
|
||||||
return false;
|
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();
|
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||||
if (static_cast<const void*>(&vao) != snap.vao || vao.GetConfigVersion() != snap.vaoConfigVersion) {
|
const Bool vaoMoved =
|
||||||
return false;
|
static_cast<const void*>(&vao) != snap.vao || vao.GetConfigVersion() != snap.vaoConfigVersion;
|
||||||
}
|
|
||||||
const auto& drawFbo =
|
const auto& drawFbo =
|
||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
||||||
@@ -4956,6 +4961,30 @@ void main() {
|
|||||||
}
|
}
|
||||||
const auto& programObj = m_programFactory->GetOrCreateProgram(
|
const auto& programObj = m_programFactory->GetOrCreateProgram(
|
||||||
program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags));
|
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 &&
|
if (bindsMoved &&
|
||||||
!m_uniformManager->SampledBindingsUnchanged(program, programObj, m_sampledBindingRecordsScratch)) {
|
!m_uniformManager->SampledBindingsUnchanged(program, programObj, m_sampledBindingRecordsScratch)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -5014,11 +5043,38 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Everything the full path would re-resolve is provably unchanged - or, for
|
// Everything the full path would re-resolve is provably unchanged - or, for
|
||||||
// a moved pipeline-state version, reduces to re-resolving just the pipeline
|
// a moved pipeline-state version or a changed VAO, reduces to re-resolving
|
||||||
// through the value-keyed memo against the still-active render pass. Run
|
// just the pipeline through the value-keyed memo against the still-active
|
||||||
// only the per-draw tail.
|
// render pass. Run only the per-draw tail.
|
||||||
VkPipeline pipeline = snap.pipeline;
|
VkPipeline pipeline = snap.pipeline;
|
||||||
if (renderStateMoved) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
// Same lookup the full path would do; every input (FBO + version, image
|
// Same lookup the full path would do; every input (FBO + version, image
|
||||||
// index, depth/stencil participation, image epochs, no pending clears)
|
// index, depth/stencil participation, image epochs, no pending clears)
|
||||||
// was verified unchanged above, so this is a pure cache hit on the same
|
// was verified unchanged above, so this is a pure cache hit on the same
|
||||||
@@ -5035,10 +5091,13 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Every decline is behind us: the snapshot again describes the current
|
// Every decline is behind us: the snapshot again describes the current
|
||||||
// counters, so the next draw's compare is two integer loads.
|
// counters, so the next draw's compare is two integer loads.
|
||||||
snap.renderStateVersion = renderStateVersion;
|
snap.renderStateVersion = renderStateVersion;
|
||||||
snap.bindGeneration = bindGeneration;
|
snap.bindGeneration = bindGeneration;
|
||||||
|
snap.vao = static_cast<const void*>(&vao);
|
||||||
|
snap.vaoConfigVersion = vao.GetConfigVersion();
|
||||||
snap.pipeline = pipeline;
|
snap.pipeline = pipeline;
|
||||||
if (!g_dynamicStateShadow.graphicsPipelineValid ||
|
if (!g_dynamicStateShadow.graphicsPipelineValid ||
|
||||||
g_dynamicStateShadow.graphicsPipeline != pipeline) {
|
g_dynamicStateShadow.graphicsPipeline != pipeline) {
|
||||||
|
|||||||
Reference in New Issue
Block a user