fix(device): add preferred mask + required-only fallback to GetMemoryType (#59)

GetMemoryType was a bare first-superset match that threw whenever no
memory type satisfied the requested property flags. Callers combining a
mandatory flag with a perf-only one (HOST_VISIBLE | DEVICE_LOCAL for the
descriptor heaps) would then fail allocation on any device without a
host-visible device-local heap (no resizable BAR).

It now takes a `preferred` mask distinct from `required`: a type
satisfying both is chosen first, falling back to a required-only match
when the preference is unavailable, and throwing only when even
`required` cannot be met. VulkanBuffer::Create/Resize gain an optional
trailing `preferredPropertyFlags`, and the descriptor heaps now treat
DEVICE_LOCAL as a preference on top of mandatory HOST_VISIBLE.

This does not touch any flush/barrier behaviour — coherency is not
assumed anywhere here.

Adds tests/MemoryTypeFallback, a pure-CPU test that installs synthetic
memory layouts and exercises selection, preference, fallback, and the
unsatisfiable-required throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 16:03:50 +00:00
commit 294c1378b5
6 changed files with 218 additions and 13 deletions

View file

@ -55,7 +55,12 @@ namespace Crafter {
class VulkanBuffer : public VulkanBufferBase, public VulkanBufferMappedConditional<T, Mapped> {
public:
VulkanBuffer() = default;
void Create(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
// `preferredPropertyFlags` are best-effort: the allocation prefers a
// memory type satisfying them on top of `memoryPropertyFlags`, but
// falls back to the required flags alone rather than failing. Use it
// for perf hints (e.g. DEVICE_LOCAL on a mapped buffer) that aren't
// available on every device — see Device::GetMemoryType.
void Create(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) {
size = count * sizeof(T);
// Carry usage in the maintenance5 flags2 chain so 64-bit bits
@ -77,7 +82,7 @@ namespace Crafter {
VkMemoryAllocateInfo memAlloc {
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = memReqs.size,
.memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, memoryPropertyFlags)
.memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, memoryPropertyFlags, preferredPropertyFlags)
};
VkMemoryAllocateFlagsInfoKHR allocFlagsInfo {
@ -109,11 +114,11 @@ namespace Crafter {
buffer = VK_NULL_HANDLE;
}
void Resize(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
void Resize(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) {
if(buffer != VK_NULL_HANDLE) {
Clear();
}
Create(usageFlags, memoryPropertyFlags, count);
Create(usageFlags, memoryPropertyFlags, count, preferredPropertyFlags);
}
void Copy(VkCommandBuffer cmd, VulkanBuffer& dst) {