102 lines
5.1 KiB
C++
102 lines
5.1 KiB
C++
|
|
//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 <cstdlib>
|
||
|
|
#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 <typename Allocate, typename Free>
|
||
|
|
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<std::uint16_t>(again.firstElement + 2), 1 });
|
||
|
|
fresh.FreeImageSlots({ again.firstElement, 1 });
|
||
|
|
fresh.FreeImageSlots({ static_cast<std::uint16_t>(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;
|
||
|
|
}
|