- Replace gpu-allocator with vk-mem 0.5.0 (VMA 3.3.0) in renderer_device - Add public GpuAllocation wrapper, MemoryLocation, AllocationDesc, and allocate_for_buffer/image + bind_buffer/image_memory helpers - Restrict CPU-mapped allocations to HOST_VISIBLE|HOST_COHERENT memory types - Handle guards: raw buffer/image handles owned before allocation so failure paths never leak - Use renderer_device::device::MemoryLocation across renderer, renderer_resources, and renderer_rendergraph; remove all gpu-allocator dependencies - Error on dedicated allocation below Vulkan 1.1 instead of silent downgrade - Add Send/Sync asserts, coherent-mapping and dedicated-allocation validation tests; update AGENTS.md docs
18 KiB
Migration Plan: gpu-allocator → VMA via vk-mem
Status: proposed — rev 2 (Luna review fixes applied) Date: 2026-10-26 Scope: workspace-wide memory-allocator swap
Goal
Replace the gpu-allocator crate with VMA (Vulkan Memory Allocator) via the
vk-mem bindings crate, preserving current allocation behavior (buffer/image
create/bind/destroy, mapping, dedicated allocations) and the existing real-Vulkan
tests.
Architecture decision
Requirements
- Hard: remove
gpu-allocator; usevk-mem/VMA; preserve the dependency directionbevy_vulkan_render→renderer→renderer_resources→renderer_device; keep buffer/image create/bind/destroy and mapping behavior; keep real-Vulkan tests working. - Nice: dedicated allocations, allocation naming/stats, defragmentation (not required now).
Constraints
- Nightly toolchain, edition 2024.
renderer_resourcesmust not depend onrenderer.renderer_deviceowns device/allocator state.renderer_resources/renderershould not exposevk_memtypes.- The
testingfeature gates real-device tests. - No per-frame allocation is acceptable on current paths.
renderer_rendergraphis also a workspace member and directly usesgpu_allocator::MemoryLocation, so it must be included (it is not mentioned in the workspace guide).
Current architecture
- Deps: root
Cargo.toml:45gitgpu-allocator(line 44 has a commented# vk-mem = "0.5.0");renderer_device/Cargo.toml:12,renderer_resources/Cargo.toml:12,renderer/Cargo.toml:27,renderer_rendergraph/Cargo.toml:8.Cargo.lock:3423-3435is the git package;Cargo.lock:3409-3420is the registry copy pulled bywgpu-halvia the Bevy dev-dependency. - Allocator state:
crates/renderer_device/src/device.rs:151pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>, initialized at:441-456(AllocatorCreateDesc,AllocationSizes::new(8MB,64MB)with 256MB maxima).GpuAllocationis the private alias at:111;AllocationStrategyat:114-124;Allocation::{Owned,Shared,Unmanaged}at:126-147. Free path isExternallyManagedObject<Device> for GpuAllocationat:1074-1077, which callsalloc2.lock().free(...). - Errors:
crates/renderer_device/src/lib.rs:81GpuAllocatorError(#[from] gpu_allocator::AllocationError). - Buffer flow:
crates/renderer_resources/src/buffers.rs:4importsAllocationScheme;:20,68MemoryLocation;:108-122allocates fromalloc2;:128-130binds with rawvkBindBufferMemory;:137wraps inAllocation::Owned(DeviceObject<GpuAllocation>);:164-181maps viamapped_slice/mapped_slice_mut. - Image flow:
crates/renderer_resources/src/images.rs:16imports VMA-namedAllocationCreateDesc/Scheme;:35,126,289MemoryLocation;:191-205allocates and wraps;:217-223binds;:244-260validates size/memory-type fornew_with_allocation;:418-428exposes allocation. - Dedicated logic:
buffers.rs:116-121usesAllocationScheme::DedicatedBuffer(buffer);images.rs:196-199usesDedicatedImage(image);renderer_rendergraph/src/graph.rs:1352-1357chooses dedicated at 64 MiB. - Direct test allocations:
crates/renderer_device/src/testing.rs:312-319and:351-357allocate/free directly throughdevice.alloc2. - Other type consumers:
renderer/src/rendering/mod.rs:8,56,77,87,97;renderer/src/egui.rs:8,74-75,140,157,510,531,538,545,555;renderer/src/render_graph/graph_builder.rs:51,60,69,87,168-183;renderer_rendergraph/src/resources.rs:16,27,46,69; testsrenderer_resources/tests/managed_set_validation.rs:26andrenderer_rendergraph/tests/graph_validation.rs:31. - No stats/reporting or defragmentation call sites exist today.
Buffer/Imagedrop field order: buffer/image handle first, then allocation (buffers.rs:74-78,images.rs:144-148).
Alternatives considered
- Swap allocator inside
renderer_device, add a private wrapper and a workspace-ownedMemoryLocation(chosen): least invasive, upper crates stay VMA-free. - Re-export
vk_memtypes as public resource API: least code, but leaks VMA intorenderer_resources/rendererand locks the workspace API tovk-mem. - Use
vk_mem::create_buffer/create_imagedirectly inrenderer_resources: fewer Vulkan calls and idiomatic VMA, but larger ownership refactor and couples the resource crate tovk_mem. - Add a memory-backend trait: easy A/B testing, but unnecessary indirection and lifetime complexity for a one-time swap.
Decision rationale (feasibility verified)
vk-mem 0.5.0bundles VMA 3.3.0 and depends on ash 0.38 (workspace ash is0.38.0+1.3.281), so there is one ash and no duplicate-ash problem.vk_mem::AllocatorisSend + Syncand internally synchronized unlessEXTERNALLY_SYNCHRONIZEDis set; keep theMutex.vk_mem::AllocationisCopy, Send, Syncand has no inherentsize/memory/offsetaccessors, so the wrapper must retainAllocationInfo.
Target design
renderer_device::device::MemoryLocation { Unknown, GpuOnly, CpuToGpu, GpuToCpu }withDefault = Unknown,Copy/Eq/Hash; use it as the publicmem_locationtype.pub struct GpuAllocation { raw: vk_mem::Allocation, memory: vk::DeviceMemory, offset: u64, size: u64, memory_type_index: u32, mapped: Option<SendSyncPtr> }.SendSyncPtris a privateNonNull<u8>wrapper with unsafeSend/Sync, mirroringgpu_allocator's pointer wrapper. ImplementDebug,memory()/offset()/size()/memory_type_index()/mapped_slice()/mapped_slice_mut().pub struct AllocationDesc<'a> { name: &'a str, location: MemoryLocation, strategy: AllocationStrategy }andDeviceInner::{allocate_for_buffer, allocate_for_image, bind_buffer_memory, bind_image_memory}. Note the call shapes:allocate_memory_for_buffer/imagelive on thevk_mem::Alloctrait (importvk_mem::Alloc as _) and areunsafeinvk-mem 0.5.0;bind_buffer_memory/bind_image_memoryare inherent (alsounsafe)Allocatormethods, not trait methods.DeviceInner.alloc2: Mutex<vk_mem::Allocator>remains the single allocator owner. KeepAllocation::{Owned(DeviceObject<GpuAllocation>), Shared(Arc<...>), Unmanaged}unchanged in shape.renderer_resources/renderer/renderer_rendergraphuse onlyrenderer_device::device::MemoryLocation,Allocation, and the newDevicehelpers; novk_memimports outsiderenderer_device.
Suggested interface:
impl DeviceInner {
pub fn allocate_for_buffer(&self, buffer: vk::Buffer, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
pub fn allocate_for_image(&self, image: vk::Image, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
pub fn bind_buffer_memory(&self, alloc: &GpuAllocation, buffer: vk::Buffer) -> Result<()>;
pub fn bind_image_memory(&self, alloc: &GpuAllocation, image: vk::Image) -> Result<()>;
}
Dependency changes
- Root
Cargo.toml: addvk-mem = "0.5.0"to[workspace.dependencies](the commented line at:44anticipates this); remove the gitgpu-allocatorentry in phase 3. renderer_device/Cargo.toml: addvk-mem = { workspace = true }; removegpu-allocatorin phase 2 (after the swap compiles).renderer_resources/Cargo.toml,renderer/Cargo.toml,renderer_rendergraph/Cargo.toml: removegpu-allocatorin phase 3.- Ash alignment:
vk-mem 0.5.0pins ash 0.38; an ash upgrade requires a matchingvk-memrelease or fork.
API mapping
| gpu-allocator | VMA / vk-mem |
|---|---|
AllocatorCreateDesc instance/device/physical_device |
AllocatorCreateInfo::new(&instance, &device, physical_device) |
debug_settings, allocation sizes |
VMA compile-time debug macros; preferred_large_heap_block_size only (loss of min/max block tuning) |
buffer_device_address |
AllocatorCreateFlags::BUFFER_DEVICE_ADDRESS (off here) |
MemoryLocation::{Unknown,GpuOnly,CpuToGpu,GpuToCpu} |
MemoryUsage::{Unknown,GpuOnly,CpuToGpu,GpuToCpu} |
AllocationScheme::GpuAllocatorManaged |
default allocation (AllocationCreateFlags::empty()) |
DedicatedBuffer(buffer) / DedicatedImage(image) |
DEDICATED_MEMORY + allocate_memory_for_buffer/image (passes VkMemoryDedicatedAllocateInfo when API version ≥ 1.1) |
AllocationCreateDesc.linear |
implicit: use allocate_memory_for_buffer/image, not generic allocation |
Allocation.memory()/offset()/size()/memory_type_index() |
AllocationInfo.device_memory/offset/size/memory_type captured at allocation |
mapped_slice()/mapped_slice_mut() |
AllocationInfo.mapped_data from MAPPED allocation, or Allocator::map_memory |
Allocator::free(allocation) |
Allocator::free_memory(&mut allocation) |
bind_buffer_memory/bind_image_memory |
Allocator::{bind_buffer_memory, bind_image_memory} |
| stats report | calculate_statistics() / get_heap_budgets(); no allocation names in vk-mem 0.5.0 |
| debug names | unchanged: DeviceObject::new_debug_named sets Vulkan object names; VMA allocation names are not exposed |
Migration phases
Phase 0 — baseline
Run cargo check --workspace, cargo test -p renderer_device --features testing, inspect git status. No edits.
Phase 1 — dependency/type scaffolding (non-breaking)
Add vk-mem = "0.5.0" to root [workspace.dependencies]; add
vk-mem = { workspace = true } to renderer_device/Cargo.toml; add
MemoryLocation, GpuAllocation skeleton, and AllocationDesc in
crates/renderer_device/src/device.rs while leaving the old allocator
untouched.
Verify: cargo check -p renderer_device.
Phase 2 — atomic allocator swap (one compile unit)
In crates/renderer_device/src/device.rs replace alloc2/creation/
GpuAllocation alias/free impl with VMA; remove GpuAllocatorError from
crates/renderer_device/src/lib.rs:81; rewrite testing.rs:312-319,351-357;
in crates/renderer_resources/src/buffers.rs and images.rs replace
alloc2.allocate + raw bind with allocate_for_buffer/image + bind_*, adding
handle guards in the same pass (raw handle wrapped in DeviceObject immediately
after creation, allocation ownership established before binding — see
implementation notes), but temporarily keep the public fields as
gpu_allocator::MemoryLocation and convert at the call site.
Verify: cargo check --workspace and cargo test -p renderer_device --features testing.
Phase 3 — public type/dependency cleanup
Change BufferDesc.mem_location/ImageDesc.mem_location to
renderer_device::device::MemoryLocation; update renderer/src/rendering/mod.rs,
renderer/src/egui.rs, renderer/src/render_graph/graph_builder.rs,
renderer_rendergraph/src/resources.rs, and both tests
(renderer_resources/tests/managed_set_validation.rs,
renderer_rendergraph/tests/graph_validation.rs); remove gpu-allocator from
renderer_resources, renderer, renderer_rendergraph, and root manifests.
Verify: cargo check --workspace; rg -n 'gpu_allocator|gpu-allocator' --glob '!Cargo.lock' should be empty.
Phase 4 — hardening/docs
- Ensure
GpuAllocationisSend + Sync. - Add handle guards: wrap raw buffer/image handles in
DeviceObjectright after creation and establish allocation ownership before binding, so allocation and bind failures never leak (see implementation notes). - Add a validation test for
CpuToGpumapping that asserts the allocated memory type isHOST_VISIBLE | HOST_COHERENTand round-trips a CPU write throughmap_mut, plus dedicated buffers/images. - Update
crates/renderer_device/AGENTS.md,crates/renderer_resources/AGENTS.md,crates/renderer/AGENTS.md, and the root workspace guide to say VMA/vk-mem. - Optionally rename
alloc2toallocator.
Verify: full checklist below.
Implementation notes
- Allocator creation:
unsafe { vk_mem::Allocator::new(create_info) }?; setpreferred_large_heap_block_sizeto ~64 MiB (VMA's default is 256 MiB).vulkan_api_versionmust not exceed the instance's requested version or the selected physical device's supported version: record the instance request (addpub api_version: u32toInstanceInnerwhen buildingApplicationInfoininstance.rs:126-135) and clamp it with the physical device'slimits.api_versionat device selection, passing the effective minimum (a safe floor is 1.1 when both sides support it, else 1.0). Dedicated-allocation structs require API ≥ 1.1; on Vulkan 1.0 devices requireVK_KHR_dedicated_allocation(+VK_KHR_get_memory_requirements2) at device creation or skipDEDICATED_MEMORYthere. - Do not enable
EXTERNALLY_SYNCHRONIZED; keep theMutex. - Allocation flags:
MAPPEDforCpuToGpu/GpuToCpu, plusDEDICATED_MEMORYforAllocationStrategy::Dedicated. - Coherence policy (required): VMA's
CpuToGpu/GpuToCpuusages guarantee host-visible memory, not host-coherent memory, but the current mapping API (mapped_slice/mapped_slice_mut) has no flush/invalidate step. Require coherent memory: compute the memory-type bitmask of types withHOST_VISIBLE | HOST_COHERENTfrom the physical device's memory properties and setAllocationCreateInfo.memory_type_bitsfor CPU-mapped allocations; fail the allocation if no such type exists (stricter than gpu-allocator, which silently falls back to non-coherent types). If a non-coherent-only device must be supported later, switch to explicitAllocator::flush_allocation/invalidate_allocationaround map windows and change the API contract accordingly — do not ship a silent fallback. - Handle guards before allocation: the buffer/image flows create raw Vulkan
handles before allocating (
buffers.rs:108-130,images.rs:191-223); wrap each raw handle inDeviceObjectimmediately after creation and establishAllocationownership (DeviceObject<GpuAllocation>) before binding, so every failure path — allocation failure or bind failure — releases the handle and the allocation in the correct order (destroy handle first, thenfree_memory). No raw handle may escape a fallible span unowned. - Free with
free_memoryafter the buffer/image handle is destroyed; do not also calldestroy_buffer/destroy_imagefor the same allocation.
Validation checklist
cargo check -p renderer_device,-p renderer_resources,-p renderer,-p renderer_rendergraph, thencargo check --workspace.cargo test -p renderer_deviceandcargo test -p renderer_device --features testing(real Vulkan/validation; skips if unavailable).cargo test -p renderer_resourcesandcargo test -p renderer_rendergraph(both exerciseTestVulkanthrough their dev-dependency features).cargo tree -d | rg '^ash'should show one ash. The lockfile has twogpu-allocator 0.28.0packages (registry + git), socargo tree -i gpu-allocatoris ambiguous; use source-qualified package IDs:cargo tree -i 'registry+https://github.com/rust-lang/crates.io-index#gpu-allocator@0.28.0'should list onlywgpu-halvia the Bevy dev-dependency, andcargo tree -i 'https://github.com/janis-bhm/gpu-allocator#gpu-allocator@0.28.0'should be empty after phase 3.rg -n 'gpu_allocator|gpu-allocator' --glob '!Cargo.lock'should return nothing.- Re-run the baseline Vulkan smoke tests and, if a display is available,
cargo build --examples.
Risks & gotchas
- FFI/lifetimes:
vk_mem::Allocatoris a raw-pointer owner; it must outlive everyAllocation. The existingDeviceInnerfield order (alloc2before_drop) is correct; do not move the allocator after_drop.GpuAllocationmust not beCopyand must free exactly once throughDeviceObject/ExternallyManagedObject. - Thread safety:
AllocationInfo.mapped_datais*mut c_void, which is notSend/Sync; storing it directly breaksArc<Buffer>thread-safety. Use a privateSendSyncPtror unsafeSend/Syncwrapper and add a compile-timeassert_send_sync::<Buffer>()/Imagetest. - Mapping differs:
gpu-allocatormapped host-visible blocks opportunistically; VMA maps only withMAPPED.CpuToGpu/GpuToCpukeep working;GpuOnlymapping on integrated GPUs may now returnNone. Coherence: VMA only guarantees host-visible (not host-coherent) memory for CPU usages; this plan requires coherent types viamemory_type_bits(see implementation notes) so the existing flush-freemap/map_mutusage stays correct. Non-coherent support would need explicitflush_allocation/invalidate_allocation; current code does not flush/invalidate either. - Dedicated allocation: use
allocate_memory_for_buffer/imagewithDEDICATED_MEMORY; genericallocate_memorywould create a separate block without dedicated buffer/image info.VkMemoryDedicatedAllocateInfois only emitted when the runtime API version is ≥ 1.1 (or the KHR dedicated extensions are enabled); pass the clamped effective version per the implementation notes and require the extensions on Vulkan 1.0 devices. - OOM handling:
gpu_allocator::AllocationErrorvariants disappear; VMA returnsvk::Result(ERROR_OUT_OF_DEVICE_MEMORY,ERROR_FEATURE_NOT_PRESENT, etc.), already handled byError::Result. No call site pattern-matches the old variant. - Build environment:
vk-mem 0.5.0compiles VMA C++ (wrapper.cpp, C++17, links stdc++ on Linux); ensure CI has a C++ compiler. It pins ash 0.38; an ash upgrade requires a matchingvk-memrelease/fork. The crate is less actively maintained thangpu-allocator. - Lost features: no
vk_memallocation naming setter (vmaSetAllocationName) and nobuild_stats_stringin 0.5.0; VMA stats/heap budgets remain available. Defragmentation is intentionally out of scope; if enabled later, cachedmemory/offset/mapped_datain the wrapper must be re-queried. - Pre-existing hazard:
Image::new_with_allocationstill creates the raw image before validation (images.rs:235-260); the handle-guard requirement (implementation notes) covers this by owning the raw image inDeviceObjectfrom creation onward.