//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // DescriptorHeapVulkan hands freed slots out again, ranges included. Freed // slots went to a free list that only a request for one slot looked at, so a // range always took slots never used before: a model that allocates its // textures and samplers as ranges, switched for another and back, used up // the heap (32 samplers last a handful of switches) and then threw "out of // slots". The test frees and allocates ranges of each kind many times over, // more slots in all than the heap has; a range reuses a freed run of slots // when one is long enough, and a long enough request after small frees // still takes fresh slots rather than failing. // // Needs a Vulkan device and no window. #include #include "vulkan/vulkan.h" import Crafter.Graphics; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // Allocates a range, frees it, `rounds` times over, with a single slot held // throughout so the range isn't simply the next free one at the top. template std::string Churn(std::uint16_t count, std::uint32_t rounds, Allocate allocate, Free free) { try { for (std::uint32_t i = 0; i < rounds; ++i) { auto range = allocate(count); if (range.count != count) return std::format("round {}: {} slots instead of {}", i, range.count, count); free(range); } } catch (const std::exception& e) { return e.what(); } return ""; } } // namespace int main() { Device::Initialize(); DescriptorHeapVulkan heap; constexpr std::uint16_t kImages = 16; constexpr std::uint16_t kBuffers = 16; constexpr std::uint16_t kSamplers = 8; heap.Initialize(kImages, kBuffers, kSamplers); // ── Ranges freed and allocated again, far more than fit ───────────────── const std::string images = Churn(3, 100, [&](std::uint16_t n) { return heap.AllocateImageSlots(n); }, [&](ImageSlotRange r) { heap.FreeImageSlots(r); }); Check(images.empty(), std::format("100 image ranges of 3, each freed, fit a heap of 16 ({})", images.empty() ? "they did" : images)); const std::string buffers = Churn(4, 100, [&](std::uint16_t n) { return heap.AllocateBufferSlots(n); }, [&](BufferSlotRange r) { heap.FreeBufferSlots(r); }); Check(buffers.empty(), std::format("and buffer ranges of 4 ({})", buffers.empty() ? "they did" : buffers)); const std::string samplers = Churn(2, 100, [&](std::uint16_t n) { return heap.AllocateSamplerSlots(n); }, [&](SamplerSlotRange r) { heap.FreeSamplerSlots(r); }); Check(samplers.empty(), std::format("and sampler ranges of 2, in a heap of 8 ({})", samplers.empty() ? "they did" : samplers)); // ── A freed run comes back as the range ───────────────────────────────── { DescriptorHeapVulkan fresh; fresh.Initialize(kImages, kBuffers, kSamplers); const ImageSlotRange first = fresh.AllocateImageSlots(3); const ImageSlotRange held = fresh.AllocateImageSlots(1); fresh.FreeImageSlots(first); const ImageSlotRange again = fresh.AllocateImageSlots(3); Check(again.firstElement == first.firstElement && again.count == 3, std::format("a range of 3 reuses the 3 slots freed before it ({} then {})", first.firstElement, again.firstElement)); // Freed one by one and out of order, they still make a run. fresh.FreeImageSlots({ static_cast(again.firstElement + 2), 1 }); fresh.FreeImageSlots({ again.firstElement, 1 }); fresh.FreeImageSlots({ static_cast(again.firstElement + 1), 1 }); const ImageSlotRange pieced = fresh.AllocateImageSlots(3); Check(pieced.firstElement == first.firstElement, std::format("as do 3 freed one by one, out of order ({})", pieced.firstElement)); // Two freed slots apart from each other make no run of two: fresh ones. const ImageSlotRange a = fresh.AllocateImageSlots(1); const ImageSlotRange b = fresh.AllocateImageSlots(1); const ImageSlotRange c = fresh.AllocateImageSlots(1); fresh.FreeImageSlots(a); fresh.FreeImageSlots(c); const ImageSlotRange pair = fresh.AllocateImageSlots(2); Check(pair.firstElement != a.firstElement && pair.firstElement != c.firstElement && pair.firstElement + 1 != a.firstElement && pair.firstElement + 1 != c.firstElement, std::format("two freed slots apart make no range of 2; it takes others ({}, {} freed; got {})", a.firstElement, c.firstElement, pair.firstElement)); (void)held; (void)b; } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }