[Fix] (Magma): a frame slot's next descriptor pool doubles its LARGEST pool of the same update-after-bind flavour, capped at 4096 sets - doubling the first (oldest, smallest) one made every grown pool the same size, so rd12's 15,016-set loading frame grew one slot to 60 pools (1x128 + 59x256); now 9 (64 ... 4096, 4096, 4096)

This commit is contained in:
2026-09-29 10:34:15 -04:00
parent 9c32d9fa68
commit c2de6576ad
2 changed files with 51 additions and 17 deletions
@@ -2137,32 +2137,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets) {
const Uint32 minSets = std::max<Uint32>(1, initialMaxSets);
const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets);
Uint32 largest = 0;
for (const auto& pool : pools) {
if (pool.updateAfterBind == updateAfterBind) {
largest = std::max(largest, pool.maxSets);
}
}
if (largest == 0) {
return minSets;
}
const Uint64 doubled = static_cast<Uint64>(largest) * 2;
return static_cast<Uint32>(std::clamp<Uint64>(doubled, minSets, maxSets));
}
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
if (frame.descriptorPools.empty()) {
return false;
}
const auto matchingBucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
? std::max<Uint32>(1, matchingBucket->maxSets)
: m_setsPerFrame;
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
: currentMaxSets;
Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame);
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
// A large pool is a large driver allocation; if the device refuses one, settle for less
// rather than failing the draw - halving down to the base size, where the old policy lived.
while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
if (grownMaxSets <= m_setsPerFrame) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)",
grownMaxSets);
return false;
}
grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2);
}
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
MGLOG_D(
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
frameIndex, currentMaxSets, grownMaxSets, frame.descriptorPools.size());
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), "
"poolCount=%zu",
frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size());
return true;
}
@@ -130,13 +130,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private:
// ---- per-frame descriptor pools ----------------------------------------------------
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
// updateAfterBind flavour, because a set can only come from a pool created with the flag
// its layout was.
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
};
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
// smallest one, and doubling IT made every grown pool the same size.
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets);
private:
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the