feat(device): runtime upload-strategy helper PreferDirectDeviceWrite (#89)

Add Device::PreferDirectDeviceWrite(size) so the memory-placement cluster
(#65/#72/#73/#75) no longer re-derives where a CPU-written, GPU-read buffer
should live. It picks the strategy at runtime from Device::memoryProperties:

  - No DEVICE_LOCAL|HOST_VISIBLE type (no resizable BAR) -> false (must stage).
  - ReBAR/UMA (BAR heap >= 90% of largest DEVICE_LOCAL heap) -> true: map and
    write directly; a staging copy would be pure overhead.
  - Small BAR window (BAR heap << VRAM) -> true only for buffers within a
    per-window budget (1/8 of the window); large buffers stage so they don't
    exhaust the window (#58).

Complements GetMemoryType (#59): #59 picks the memory *type*, this picks the
*strategy*. Upload-only — readback (#74) wants HOST_CACHED and stays separate.
A VK_EXT_memory_budget-driven remaining-space check is noted as a follow-up.

Tested by tests/UploadStrategy (pure CPU over synthetic ReBAR / UMA /
small-window / no-BAR layouts, no GPU device needed), mirroring
MemoryTypeFallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 17:23:27 +00:00
commit cfbe181d9e
4 changed files with 275 additions and 0 deletions

View file

@ -0,0 +1,173 @@
/*
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 <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;
}
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<TypeSpec> types,
std::initializer_list<HeapSpec> heaps) {
Device::memoryProperties = {};
Device::memoryProperties.memoryTypeCount = static_cast<std::uint32_t>(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<std::uint32_t>(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;
}
}
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;
}