diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index dce2c9b8..c8c5b51d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -2348,6 +2348,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { pipelineLayoutInfo.pSetLayouts = &entry.descriptorSetLayout; VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &entry.pipelineLayout), "ProgramFactory::ReflectLayout, vkCreatePipelineLayout"); + + // Built here rather than where bindingKinds is sized: at that point the vector is only + // zero-initialised and the kinds are assigned further down, so a list built there would be + // empty. Ascending by construction because the index walks upward. + entry.activeBindings.clear(); + for (Uint32 binding = 0; binding < static_cast(entry.bindingKinds.size()); ++binding) { + if (entry.bindingKinds[binding] != DescriptorBindingKind::None) { + entry.activeBindings.push_back(binding); + } + } } const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram( diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index eb0adfd0..1ca18d35 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -67,6 +67,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; Vector bindingKinds; + // The bindings this program actually declares, ascending. bindingKinds is sized to the + // 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was + // scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes + // pDynamicOffsets in binding order and the writer pushes them in iteration order, so an + // unordered list would silently mis-pair dynamic offsets with their uniform blocks. + Vector activeBindings; Vector dynamicBindings; Vector uniformBlockIndexByBinding; // Descriptor count per binding (1 except for UBO instance arrays, which occupy one @@ -114,6 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { descriptorSetLayout = other.descriptorSetLayout; pipelineLayout = other.pipelineLayout; bindingKinds = std::move(other.bindingKinds); + activeBindings = std::move(other.activeBindings); dynamicBindings = std::move(other.dynamicBindings); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); bindingDescriptorCounts = std::move(other.bindingDescriptorCounts); @@ -162,6 +169,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { descriptorSetLayout = other.descriptorSetLayout; pipelineLayout = other.pipelineLayout; bindingKinds = std::move(other.bindingKinds); + activeBindings = std::move(other.activeBindings); dynamicBindings = std::move(other.dynamicBindings); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); bindingDescriptorCounts = std::move(other.bindingDescriptorCounts); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index c2df6944..5d830302 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -1008,7 +1008,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } - const Uint64 descriptorCount64 = static_cast(maxSets) * static_cast(m_maxBindings); + // Sized from what a real program declares, not from the 256-binding cap. A GL program's + // single descriptor set holds the bindings shader reflection found - typically 2 to 8 - so + // scaling by m_maxBindings declared 5 x 64 x 256 = 81,920 descriptors per pool and 245,760 + // across the three frames in flight, which drivers that reserve backing store proportional + // to the declared count pay for at init. An outlier program is absorbed by the existing + // VK_ERROR_OUT_OF_POOL_MEMORY -> GrowFrameDescriptorPool path: pool sizes are aggregate + // budgets rather than per-set limits, and vkAllocateDescriptorSets is spec-required to + // report that error rather than fail hard. + static constexpr Uint32 kEstimatedBindingsPerSet = 8; + const Uint64 descriptorCount64 = + static_cast(maxSets) * static_cast(std::min(m_maxBindings, kEstimatedBindingsPerSet)); if (descriptorCount64 > static_cast(std::numeric_limits::max())) { MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow"); return false; @@ -1187,13 +1197,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { texelBufferViews.reserve(m_maxBindings); dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra); - const Uint32 bindingCount = - std::min(m_maxBindings, static_cast(programObj.bindingKinds.size())); - for (Uint32 binding = 0; binding < bindingCount; ++binding) { - const auto kind = programObj.bindingKinds[binding]; - if (kind == ProgramFactory::DescriptorBindingKind::None) { - continue; + // 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) { + if (binding >= m_maxBindings) { + break; // ascending, so nothing past the cap can follow } + const auto kind = programObj.bindingKinds[binding]; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 6f5e9289..ccbe37a1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -242,8 +242,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkBufferManager::TrackLiveResource(const SharedPtr& resource) { - if (m_liveResources.size() >= kLiveResourcePruneThreshold) { + // Sweep on a doubling watermark rather than on every insert past the threshold. The old + // form walked the whole vector for each new buffer once the list passed 256, and when the + // buffers are all live the walk removes nothing and the list grows by one - so creating N + // live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep + // still removes exactly the expired entries, just less often and with the same bound on how + // much dead weight can accumulate (at most as many entries as were live at the last sweep). + if (m_liveResources.size() >= std::max(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) { std::erase_if(m_liveResources, [](const WeakPtr& weak) { return weak.expired(); }); + m_liveResourcesLastPruned = m_liveResources.size(); } m_liveResources.push_back(resource); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index c1e85461..25da3f6f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -154,6 +154,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector> m_deferredBufferReleases; Vector>> m_deferredResourceReleases; Vector> m_liveResources; + // Size m_liveResources had just after the last sweep; the next sweep waits for it to double. + SizeT m_liveResourcesLastPruned = 0; Uint32 m_currentFrameIndex = 0; Uint64 m_frameSerial = 1; Uint64 m_completedSerialFloor = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a133d29d..094f6e83 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -8285,15 +8285,23 @@ void main() { resource->aspect, baseMipLevel, 1); MOBILEGL_ASSERT(srcReady, "%s: failed to transition base mip level to transfer source", __func__); - for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { - VkImageLayout dstMipLayout = originalLayout; - Bool dstReady = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, resource->image, dstMipLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + // Every generated level starts from originalLayout and ends up TRANSFER_DST_OPTIMAL, and + // the loop below only ever moves a level OUT of that layout after it has been written - so + // the whole range can be prepared in one barrier instead of one per level. That turns a + // 12-level chain's 3(N-1)+1 barrier commands into 2(N-1)+2. Each level is still + // individually transitioned to TRANSFER_SRC before it is read, so the write-then-read + // dependency between consecutive levels is unchanged. + if (generateMipLevelCount > baseMipLevel + 1) { + VkImageLayout dstRangeLayout = originalLayout; + const Bool dstRangeReady = VkTextureManager::TransitionImageLayout( + frame.commandBuffer, resource->image, dstRangeLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, originalSrcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, - resource->aspect, level, 1); - MOBILEGL_ASSERT(dstReady, "%s: failed to transition mip level %u to transfer destination", __func__, level); + resource->aspect, baseMipLevel + 1, generateMipLevelCount - (baseMipLevel + 1)); + MOBILEGL_ASSERT(dstRangeReady, "%s: failed to transition mip levels to transfer destination", __func__); + } + for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1); const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level); @@ -9085,10 +9093,26 @@ void main() { if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE) { return true; } - // The serial was submitted but has not been observed complete. Frame - // fences are only waited on when their slot is reused, so the simplest - // safe wait is to drain the graphics queue; this over-waits (bounded - // by the in-flight frame count) but never deadlocks. + // Every submission is recorded with the frame serial it was made under, so the wait can be + // narrowed to the first submission at or past the requested serial instead of draining the + // whole queue. OnSubmitsCompletedUpTo calls NotifyFrameSerialComplete for every record it + // retires, so the completed-serial floor still advances correctly after one fence wait. + for (const auto& record : m_inFlightSubmits) { + if (record.frameSerial < serial || record.fence == VK_NULL_HANDLE) { + continue; + } + if (vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS) { + break; // fall through to the drain below + } + OnSubmitsCompletedUpTo(record.submitIndex); + // Deliberately no NotifyDeviceIdle() here: that claims every submission has retired, + // which is only true after a real queue drain. Work past this record may still run. + TryDrainFrameTransients(); + return true; + } + + // No usable record - fall back to draining the graphics queue. This over-waits (bounded by + // the in-flight frame count) but never deadlocks. const VkResult result = vkQueueWaitIdle(m_graphicsQueue); if (result != VK_SUCCESS) { MGLOG_E("WaitForFrameSerial: vkQueueWaitIdle returned %d", result);