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

@ -840,4 +840,49 @@ std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags req
}
throw std::runtime_error("Could not find a matching memory type");
}
bool Device::PreferDirectDeviceWrite(VkDeviceSize size) {
constexpr VkMemoryPropertyFlags directFlags =
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
// 1. Find a DEVICE_LOCAL | HOST_VISIBLE type and remember its heap. Without
// one (no resizable BAR), direct writes are impossible -> must stage.
bool haveDirectType = false;
std::uint32_t barHeapIndex = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) {
if ((memoryProperties.memoryTypes[i].propertyFlags & directFlags) == directFlags) {
haveDirectType = true;
barHeapIndex = memoryProperties.memoryTypes[i].heapIndex;
break;
}
}
if (!haveDirectType) {
return false;
}
// 2. Compare the host-visible device-local heap against the largest plain
// DEVICE_LOCAL heap (the true VRAM size).
VkDeviceSize barHeap = memoryProperties.memoryHeaps[barHeapIndex].size;
VkDeviceSize vram = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryHeapCount; ++i) {
if (memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
vram = std::max(vram, memoryProperties.memoryHeaps[i].size);
}
}
// 3. ReBAR / UMA: the BAR heap covers (almost) all of VRAM -> direct.
// barHeap >= 0.9 * vram, written to dodge floating point / overflow.
if (barHeap >= vram - vram / 10) {
return true;
}
// 4. Small BAR window: direct only for buffers small enough that several
// still fit the window — large buffers stage so they don't exhaust it.
// The budget is tied to the actual window size rather than a magic
// constant so it adapts across devices (a fixed cap would be wrong for
// both a 256 MiB and a 4 GiB window). VK_EXT_memory_budget would refine
// this to the window's *remaining* space; see the header note.
constexpr VkDeviceSize barWindowFraction = 8;
return size <= barHeap / barWindowFraction;
}