/* 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 #89: Device::PreferDirectDeviceWrite is the runtime // upload-strategy helper that decides whether a CPU-written, GPU-read buffer // should be written directly into a HOST_VISIBLE | DEVICE_LOCAL allocation // (true) or staged through a HOST_VISIBLE buffer + vkCmdCopyBuffer into pure // DEVICE_LOCAL (false). The choice is platform-dependent: // - ReBAR / UMA: the host-visible device-local heap is ~all of VRAM, so // staging is pure overhead -> direct. // - non-ReBAR discrete: HOST_VISIBLE | DEVICE_LOCAL is a small BAR window on // a separate heap; large buffers must stage (#58), small/hot ones may map. // - no host-visible device-local type at all -> staging is mandatory. // // The decision is pure CPU logic over Device::memoryProperties (both the // memory types and the heaps), so this test installs synthetic layouts and // drives it directly — no GPU device needed at runtime, mirroring // MemoryTypeFallback. #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 auto HOST_CACHED = VK_MEMORY_PROPERTY_HOST_CACHED_BIT; constexpr VkDeviceSize KiB = 1024; constexpr VkDeviceSize MiB = 1024 * KiB; constexpr VkDeviceSize GiB = 1024 * MiB; struct TypeSpec { VkMemoryPropertyFlags flags; std::uint32_t heapIndex; }; struct HeapSpec { VkDeviceSize size; VkMemoryHeapFlags flags; }; // Install a synthetic memory layout: explicit types (with their heap index) // and explicit heaps (size + flags). void SetMemory(std::initializer_list types, std::initializer_list heaps) { Device::memoryProperties = {}; Device::memoryProperties.memoryTypeCount = static_cast(types.size()); std::uint32_t i = 0; for (const TypeSpec& t : types) { Device::memoryProperties.memoryTypes[i].propertyFlags = t.flags; Device::memoryProperties.memoryTypes[i].heapIndex = t.heapIndex; ++i; } Device::memoryProperties.memoryHeapCount = static_cast(heaps.size()); std::uint32_t h = 0; for (const HeapSpec& s : heaps) { Device::memoryProperties.memoryHeaps[h].size = s.size; Device::memoryProperties.memoryHeaps[h].flags = s.flags; ++h; } // PreferDirectDeviceWrite reads the cache CacheUploadStrategy derives from // memoryProperties (device creation does this once); refresh it here since // the test mutates the layout directly. Device::CacheUploadStrategy(); } constexpr VkMemoryHeapFlags HEAP_DEVICE_LOCAL = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; } // namespace int main() { // --- ReBAR: full 24 GiB heap is DEVICE_LOCAL | HOST_VISIBLE (this dev // machine, type 4 on heap 0) -> always direct, even for huge buffers -- { SetMemory( { {DEVICE_LOCAL, 0}, {HOST_VISIBLE | HOST_COHERENT, 1}, {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 0} }, { {24 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} }); Check(Device::PreferDirectDeviceWrite(8), "ReBAR: a tiny buffer maps directly"); Check(Device::PreferDirectDeviceWrite(8 * GiB), "ReBAR: even a multi-GiB buffer maps directly (no overhead staging)"); } // --- UMA: a single heap with all flags -> direct ---------------------- { SetMemory( { {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 0} }, { {125 * GiB, HEAP_DEVICE_LOCAL} }); Check(Device::PreferDirectDeviceWrite(64 * GiB), "UMA: single all-flags heap maps directly"); } // --- non-ReBAR discrete: small 256 MiB BAR window (heap 1) separate from // an 8 GiB VRAM heap (heap 0) -> stage large, map small ------------ { SetMemory( { {DEVICE_LOCAL, 0}, // pure VRAM {HOST_VISIBLE | HOST_COHERENT, 2}, // system RAM {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, // BAR window { {8 * GiB, HEAP_DEVICE_LOCAL}, // heap 0: VRAM {256 * MiB, HEAP_DEVICE_LOCAL}, // heap 1: BAR {32 * GiB, 0} }); // heap 2: RAM Check(!Device::PreferDirectDeviceWrite(128 * MiB), "small BAR: a large buffer stages (would exhaust the 256 MiB window)"); Check(Device::PreferDirectDeviceWrite(8 * MiB), "small BAR: a small/hot buffer maps directly into the window"); // Budget is 1/8 of the window = 32 MiB. Boundary checks. Check(Device::PreferDirectDeviceWrite(32 * MiB), "small BAR: a buffer exactly at the per-buffer budget still maps"); Check(!Device::PreferDirectDeviceWrite(32 * MiB + 1), "small BAR: one byte over the budget stages"); } // --- no host-visible device-local type (no resizable BAR) -> must stage - { SetMemory( { {DEVICE_LOCAL, 0}, {HOST_VISIBLE | HOST_COHERENT, 1}, {HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 1} }, { {8 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} }); Check(!Device::PreferDirectDeviceWrite(8), "no DEVICE_LOCAL|HOST_VISIBLE type -> staging is mandatory even for a tiny buffer"); } // --- 90% threshold: a BAR heap that is most-but-not-all of VRAM counts as // ReBAR; one well below the threshold is treated as a window -------- { // BAR heap = 9.5 GiB, VRAM = 10 GiB -> 95% >= 90% -> ReBAR -> direct. SetMemory( { {DEVICE_LOCAL, 0}, {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, { {10 * GiB, HEAP_DEVICE_LOCAL}, {VkDeviceSize(9.5 * GiB), HEAP_DEVICE_LOCAL} }); Check(Device::PreferDirectDeviceWrite(4 * GiB), "BAR >= 90% of VRAM is treated as ReBAR: large buffer still maps"); // BAR heap = 2 GiB, VRAM = 10 GiB -> 20% < 90% -> window -> stage big. SetMemory( { {DEVICE_LOCAL, 0}, {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, { {10 * GiB, HEAP_DEVICE_LOCAL}, {2 * GiB, HEAP_DEVICE_LOCAL} }); Check(!Device::PreferDirectDeviceWrite(4 * GiB), "BAR well below 90% of VRAM is a window: large buffer stages"); Check(Device::PreferDirectDeviceWrite(128 * MiB), "BAR window of 2 GiB still maps a small buffer (budget = 256 MiB)"); } if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }