/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // Regression test for issue #63: VulkanBuffer::Resize used to unconditionally // destroy + reallocate. It now reuses the existing allocation in place when the // new request still fits within the created capacity AND the immutable-at-create // properties match (usage flags are fixed at create; the chosen memory type must // still satisfy the required property flags). The reuse path only shrinks `size` // and issues no Vulkan call. // // The guard is pure logic over recorded fields, so this test drives it directly // with no GPU device: it stamps a fake non-null buffer handle plus capacity / // usage / chosen-flags, then calls Resize. With Device::device == VK_NULL_HANDLE // any real destroy/reallocate would dereference a null dispatch handle and // crash — so reaching the line after a reuse Resize is the assertion that the // in-place path was taken. The realloc path (flag/size mismatch) is intentionally // not exercised here because it would issue real Vulkan calls. #include #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; } constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; constexpr VkBufferUsageFlags2 USAGE = VK_BUFFER_USAGE_2_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT; // A fake non-null, non-dereferenced handle: Resize's reuse path never touches it. VkBuffer FakeHandle() { return reinterpret_cast(static_cast(0x1)); } // Stamp a buffer into the "already created with capacity C" state without // touching the GPU, then neutralise it so the destructor's Clear() is skipped. template void NeutraliseHandle(Buf& buf) { buf.buffer = VK_NULL_HANDLE; } } // namespace int main() { Check(Device::device == VK_NULL_HANDLE, "no Vulkan device created — a real reallocate would fault"); { // Shrink within capacity, flags match → reuse in place, only size shrinks. VulkanBuffer buf; buf.buffer = FakeHandle(); buf.capacity = 16 * sizeof(float); buf.size = 16 * sizeof(float); buf.usageFlagsCreated = USAGE; buf.memoryPropertyFlagsChosen = HOST_VISIBLE | HOST_COHERENT; buf.Resize(USAGE, HOST_VISIBLE, 4); // 4 floats <= 16-float capacity Check(buf.buffer == FakeHandle(), "fitting Resize reuses the existing handle (no realloc)"); Check(buf.size == 4 * sizeof(float), "reuse updates size to the new logical extent"); Check(buf.capacity == 16 * sizeof(float), "reuse leaves capacity at the original allocation size"); NeutraliseHandle(buf); } { // Exact-fit request (size == capacity) is still a reuse. VulkanBuffer buf; buf.buffer = FakeHandle(); buf.capacity = 8 * sizeof(float); buf.size = 2 * sizeof(float); buf.usageFlagsCreated = USAGE; buf.memoryPropertyFlagsChosen = HOST_VISIBLE; buf.Resize(USAGE, HOST_VISIBLE, 8); Check(buf.buffer == FakeHandle() && buf.size == 8 * sizeof(float), "exact-capacity Resize reuses the allocation"); NeutraliseHandle(buf); } { // Reuse requires the chosen memory type to still satisfy the required // flags. Chosen type carries DEVICE_LOCAL, so a request for it is // satisfied and reuse is allowed. VulkanBuffer buf; buf.buffer = FakeHandle(); buf.capacity = 8 * sizeof(float); buf.size = 8 * sizeof(float); buf.usageFlagsCreated = USAGE; buf.memoryPropertyFlagsChosen = HOST_VISIBLE | DEVICE_LOCAL; buf.Resize(USAGE, DEVICE_LOCAL, 4); Check(buf.buffer == FakeHandle() && buf.size == 4 * sizeof(float), "reuse allowed when chosen memory type satisfies required flags"); NeutraliseHandle(buf); } if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }