feat: migrate GPU memory allocator from gpu-allocator to VMA (vk-mem)
- 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
This commit is contained in:
parent
7b2adb2c03
commit
e116851b90
|
|
@ -41,8 +41,7 @@ thread_local = "1.1.8"
|
||||||
|
|
||||||
ash = "0.38.0"
|
ash = "0.38.0"
|
||||||
ash-window = "0.13.0"
|
ash-window = "0.13.0"
|
||||||
# vk-mem = "0.5.0"
|
vk-mem = "0.5.0"
|
||||||
gpu-allocator = { git = "https://github.com/janis-bhm/gpu-allocator", branch = "main" }
|
|
||||||
rectangle-pack = "0.4.2"
|
rectangle-pack = "0.4.2"
|
||||||
# vk-sync = "0.1.6"
|
# vk-sync = "0.1.6"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
- `RenderGraph::add_resource` / `import_image` / `import_buffer` / `import_framebuffer` register graph resources; `add_pass` records read/write declarations and a callback; `resolve` creates internal resources and returns a command list with graph transitions.
|
- `RenderGraph::add_resource` / `import_image` / `import_buffer` / `import_framebuffer` register graph resources; `add_pass` records read/write declarations and a callback; `resolve` creates internal resources and returns a command list with graph transitions.
|
||||||
- `rendering::Wireframe` is an example higher-level renderer; egui pass functions are re-exported from `rendering` rather than the private `egui_pass` module.
|
- `rendering::Wireframe` is an example higher-level renderer; egui pass functions are re-exported from `rendering` rather than the private `egui_pass` module.
|
||||||
- Resource types are defined in `renderer_resources`: `Buffer`/`BufferDesc`; `Image`/`ImageDesc`, `ImageView`/`ImageViewDesc`, `MipRange`, `Extent`, and `Offset`; and pipeline, descriptor, shader, and sampler types.
|
- Resource types are defined in `renderer_resources`: `Buffer`/`BufferDesc`; `Image`/`ImageDesc`, `ImageView`/`ImageViewDesc`, `MipRange`, `Extent`, and `Offset`; and pipeline, descriptor, shader, and sampler types.
|
||||||
- Buffer/image descriptors carry Vulkan usage, memory location, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory only when the allocator returns a mapped allocation.
|
- Buffer/image descriptors carry Vulkan usage, `renderer_device::device::MemoryLocation`, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory for VMA-mapped allocations (CPU-mapped locations require host-visible, host-coherent memory).
|
||||||
- The egui integration uploads texture deltas to GPU images and records passes through the legacy `RenderGraph`; shader binaries remain under `crates/renderer/shaders`, and renderer's hardcoded shader paths are unchanged.
|
- The egui integration uploads texture deltas to GPU images and records passes through the legacy `RenderGraph`; shader binaries remain under `crates/renderer/shaders`, and renderer's hardcoded shader paths are unchanged.
|
||||||
- `ImageDesc` includes image shape, format, layout, usage, queue family selection, and allocation options. The resource crate's `Image::new_with_allocation` checks allocation size and memory-type compatibility before binding.
|
- `ImageDesc` includes image shape, format, layout, usage, queue family selection, and allocation options. The resource crate's `Image::new_with_allocation` checks allocation size and memory-type compatibility before binding.
|
||||||
- `ImageViewDesc` selects view type, format, aspects, mip range, and array layers; the resource crate's `Image::create_view` normalizes `REMAINING_MIP_LEVELS` / `REMAINING_ARRAY_LAYERS` and checks image/view compatibility.
|
- `ImageViewDesc` selects view type, format, aspects, mip range, and array layers; the resource crate's `Image::create_view` normalizes `REMAINING_MIP_LEVELS` / `REMAINING_ARRAY_LAYERS` and checks image/view compatibility.
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
- Device ownership is delegated to `renderer_device`: wrappers here strongly retain `Device` through its `DeviceObject` or explicit fields. Do not place a device-owning wrapper back inside `DeviceInner`.
|
- Device ownership is delegated to `renderer_device`: wrappers here strongly retain `Device` through its `DeviceObject` or explicit fields. Do not place a device-owning wrapper back inside `DeviceInner`.
|
||||||
- `Image::from_swapchain_raw(image, device, format, extent, usage)` wraps a swapchain-owned raw image without taking image ownership. `Swapchain::acquire_image` passes the swapchain device, configured format/usage, and configured extent with depth 1. Its descriptor remains TYPE_2D, one mip/layer, TYPE_1 samples, OPTIMAL tiling, PRESENT queue flags, UNDEFINED layout, and GpuOnly memory. Do not destroy that image as an independently allocated image.
|
- `Image::from_swapchain_raw(image, device, format, extent, usage)` wraps a swapchain-owned raw image without taking image ownership. `Swapchain::acquire_image` passes the swapchain device, configured format/usage, and configured extent with depth 1. Its descriptor remains TYPE_2D, one mip/layer, TYPE_1 samples, OPTIMAL tiling, PRESENT queue flags, UNDEFINED layout, and GpuOnly memory. Do not destroy that image as an independently allocated image.
|
||||||
- `ImageView` retains an `Arc<Image>` so its source image outlives the Vulkan view. `Image::create_view` in `renderer_resources` returns `ManuallyDrop<ImageView>`; renderer call sites transfer it deliberately (the swapchain path uses `ManuallyDrop::into_inner`).
|
- `ImageView` retains an `Arc<Image>` so its source image outlives the Vulkan view. `Image::create_view` in `renderer_resources` returns `ManuallyDrop<ImageView>`; renderer call sites transfer it deliberately (the swapchain path uses `ManuallyDrop::into_inner`).
|
||||||
- For allocated images, `Allocation::Owned` frees allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocator allocation.
|
- For allocated images, `Allocation::Owned` frees VMA allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocation.
|
||||||
- `Swapchain` owns raw swapchain images and manually created acquire/release semaphores. Its drop path waits idle and destroys those semaphores; the `DeviceObject` destroys the swapchain handle.
|
- `Swapchain` owns raw swapchain images and manually created acquire/release semaphores. Its drop path waits idle and destroys those semaphores; the `DeviceObject` destroys the swapchain handle.
|
||||||
- `SwapchainImage` / `SwapchainFrame` are `#[must_use]` acquired-frame values. Presentation consumes the frame; preserve its `Arc<Swapchain>` so image ownership stays valid until presentation.
|
- `SwapchainImage` / `SwapchainFrame` are `#[must_use]` acquired-frame values. Presentation consumes the frame; preserve its `Arc<Swapchain>` so image ownership stays valid until presentation.
|
||||||
- `Surface` uses a `DropGuard` for surface destruction and keeps its `Instance`; field order drops the swapchain lock first, then the guard, then the instance.
|
- `Surface` uses a `DropGuard` for surface destruction and keeps its `Instance`; field order drops the swapchain lock first, then the guard, then the instance.
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
- The crate root also exposes `Extent2D` and `utils` alongside renderer modules and device re-exports.
|
- The crate root also exposes `Extent2D` and `utils` alongside renderer modules and device re-exports.
|
||||||
- `SamplerDesc` implements float-aware equality and hashing with utility helpers; keep those implementations consistent if adding sampler fields.
|
- `SamplerDesc` implements float-aware equality and hashing with utility helpers; keep those implementations consistent if adding sampler fields.
|
||||||
- `Pipeline::new_graphics` supports dynamic rendering state through `RenderingState`; its render-pass and rendering choices are supplied in `GraphicsPipelineDesc`.
|
- `Pipeline::new_graphics` supports dynamic rendering state through `RenderingState`; its render-pass and rendering choices are supplied in `GraphicsPipelineDesc`.
|
||||||
- `Buffer` and `Image` creation in `renderer_resources` allocates/binds via `DeviceInner.alloc2`; device ownership and final allocation release are handled by the device-layer wrappers.
|
- `Buffer` and `Image` creation in `renderer_resources` allocates/binds through `DeviceInner::allocate_for_buffer` / `allocate_for_image` and `bind_buffer_memory` / `bind_image_memory`; device ownership and final VMA allocation release are handled by the device-layer wrappers.
|
||||||
- Resource types live in `renderer_resources`; the `renderer` aliases are crate-private and are not externally reachable as `renderer::buffers::...` etc.
|
- Resource types live in `renderer_resources`; the `renderer` aliases are crate-private and are not externally reachable as `renderer::buffers::...` etc.
|
||||||
- `Renderer2::new` selects an adapter with `None` for the surface, then creates the logical device using its retained display handle. `choose_adapter_default` does not evaluate raw surface compatibility.
|
- `Renderer2::new` selects an adapter with `None` for the surface, then creates the logical device using its retained display handle. `choose_adapter_default` does not evaluate raw surface compatibility.
|
||||||
- `render_graph/legacy.rs` implements the active public graph resolver. Its resources are ordered by `GraphResourceId`, and passes declare reads/writes through `PassDesc` so the resolver can derive dependencies and transitions.
|
- `render_graph/legacy.rs` implements the active public graph resolver. Its resources are ordered by `GraphResourceId`, and passes declare reads/writes through `PassDesc` so the resolver can derive dependencies and transitions.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ tracing = { workspace = true }
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
ash-window = { workspace = true }
|
ash-window = { workspace = true }
|
||||||
# vk-mem = { workspace = true }
|
# vk-mem = { workspace = true }
|
||||||
gpu-allocator = { workspace = true }
|
|
||||||
rectangle-pack = { workspace = true }
|
rectangle-pack = { workspace = true }
|
||||||
|
|
||||||
raw-window-handle = { workspace = true }
|
raw-window-handle = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,13 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use gpu_allocator::MemoryLocation;
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
EguiState,
|
EguiState,
|
||||||
buffers::{Buffer, BufferDesc},
|
buffers::{Buffer, BufferDesc},
|
||||||
commands::traits::CommandBufferExt,
|
commands::traits::CommandBufferExt,
|
||||||
device::{self, AllocationStrategy},
|
device::{self, AllocationStrategy, MemoryLocation},
|
||||||
images::{Image, ImageDesc, ImageViewDesc},
|
images::{Image, ImageDesc, ImageViewDesc},
|
||||||
render_graph::{
|
render_graph::{
|
||||||
Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext,
|
Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ use ash::vk;
|
||||||
use derive_more::derive::{Deref, DerefMut};
|
use derive_more::derive::{Deref, DerefMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
buffers, images,
|
buffers,
|
||||||
|
device::MemoryLocation,
|
||||||
|
images,
|
||||||
render_graph::{
|
render_graph::{
|
||||||
recorder::CommandList,
|
recorder::CommandList,
|
||||||
resources::{self, ResourceId},
|
resources::{self, ResourceId},
|
||||||
|
|
@ -48,7 +50,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: gpu_allocator::MemoryLocation,
|
_location: MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +59,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: gpu_allocator::MemoryLocation,
|
_location: MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +68,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: gpu_allocator::MemoryLocation,
|
_location: MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +86,6 @@ mod api_demo {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use ash::vk::{self, BufferUsageFlags};
|
use ash::vk::{self, BufferUsageFlags};
|
||||||
use gpu_allocator::MemoryLocation;
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
fn egui_graph<'a>(
|
fn egui_graph<'a>(
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use glam::{
|
use glam::{
|
||||||
camera::rh::{proj, view},
|
camera::rh::{proj, view},
|
||||||
vec3,
|
vec3,
|
||||||
};
|
};
|
||||||
use gpu_allocator::MemoryLocation;
|
|
||||||
|
|
||||||
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Result,
|
Result,
|
||||||
buffers::{Buffer, BufferDesc},
|
buffers::{Buffer, BufferDesc},
|
||||||
commands::{self, traits::CommandBufferExt},
|
commands::{self, traits::CommandBufferExt},
|
||||||
device::Device,
|
device::{Device, MemoryLocation},
|
||||||
images::ImageViewDesc,
|
images::ImageViewDesc,
|
||||||
pipeline,
|
pipeline,
|
||||||
render_graph::{
|
render_graph::{
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
## Key types and ownership
|
## Key types and ownership
|
||||||
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
|
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
|
||||||
- `DeviceInner` keeps the raw `ash::Device`, allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
|
- `DeviceInner` keeps the raw `ash::Device`, VMA (`vk_mem`) allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
|
||||||
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
|
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
|
||||||
- `PhysicalDeviceInfo::create_logical_device` selects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructs `DeviceInner` with `Arc::new_cyclic`.
|
- `PhysicalDeviceInfo::create_logical_device` selects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructs `DeviceInner` with `Arc::new_cyclic`.
|
||||||
- `DeviceInner::dev`, `instance`, `queues`, `physical_device`, `main_queue`, `compute_queue`, and `transfer_queue` are the main convenience accessors for downstream code.
|
- `DeviceInner::dev`, `instance`, `queues`, `physical_device`, `main_queue`, `compute_queue`, and `transfer_queue` are the main convenience accessors for downstream code.
|
||||||
|
|
@ -27,14 +27,16 @@
|
||||||
- The `DeviceRef` proc macro is implemented in the separate `renderer_macros` package and emits paths rooted at `::renderer_device::`.
|
- The `DeviceRef` proc macro is implemented in the separate `renderer_macros` package and emits paths rooted at `::renderer_device::`.
|
||||||
- `sync::Fence` and `sync::Semaphore` distinguish `Dedicated` (`DeviceObject`) from `Pooled` (`PoolObject`) ownership. `SemaphoreInner`, `BinarySemaphore`, and `TimelineSemaphore` represent semaphore-kind handles.
|
- `sync::Fence` and `sync::Semaphore` distinguish `Dedicated` (`DeviceObject`) from `Pooled` (`PoolObject`) ownership. `SemaphoreInner`, `BinarySemaphore`, and `TimelineSemaphore` represent semaphore-kind handles.
|
||||||
- `PhysicalDeviceInfo`, `PhysicalDeviceFeatures`, `PhysicalDeviceProperties`, and `SurfaceCapabilities` carry adapter/capability data. `Instance::choose_adapter_default` accepts `Option<vk::SurfaceKHR>` (a raw handle), not a swapchain/surface wrapper.
|
- `PhysicalDeviceInfo`, `PhysicalDeviceFeatures`, `PhysicalDeviceProperties`, and `SurfaceCapabilities` carry adapter/capability data. `Instance::choose_adapter_default` accepts `Option<vk::SurfaceKHR>` (a raw handle), not a swapchain/surface wrapper.
|
||||||
- `Error` and `Result<T>` are the crate-wide error types; `Error` covers Vulkan, loading, allocator, I/O, and renderer-facing resource/capability failures.
|
- `Error` and `Result<T>` are the crate-wide error types; `Error` covers Vulkan/VMA (`vk_mem`), loading, I/O, and renderer-facing resource/capability failures.
|
||||||
- `InstanceDesc` configures borrowed instance extensions/layers, optional app metadata, and a raw display handle; its default requests surface and validation layers, with validation settings in debug builds. `Instance::new` creates a shared `Instance` and requests debug-utils plus optional capability-query extensions alongside requested/platform extensions.
|
- `InstanceDesc` configures borrowed instance extensions/layers, optional app metadata, and a raw display handle; its default requests surface and validation layers, with validation settings in debug builds. `Instance::new` creates a shared `Instance` and requests debug-utils plus optional capability-query extensions alongside requested/platform extensions.
|
||||||
- `Instance::expose_adapter` queries one adapter. `choose_adapter_default` ranks enumerated adapters against requested extensions/features and prefers discrete devices, then newer APIs, device-local memory, and texture size.
|
- `Instance::expose_adapter` queries one adapter. `choose_adapter_default` ranks enumerated adapters against requested extensions/features and prefers discrete devices, then newer APIs, device-local memory, and texture size.
|
||||||
- `PhysicalDeviceFeatures` stores core, Vulkan 1.1/1.2/1.3, and optional mesh-shader feature structures; `push_to_device_create_info` chains enabled feature structs.
|
- `PhysicalDeviceFeatures` stores core, Vulkan 1.1/1.2/1.3, and optional mesh-shader feature structures; `push_to_device_create_info` chains enabled feature structs.
|
||||||
- `PhysicalDeviceProperties` stores supported extensions, core/versioned properties, optional mesh-shader properties, memory properties, and API version; `supports_extension` checks name and minimum version.
|
- `PhysicalDeviceProperties` stores supported extensions, core/versioned properties, optional mesh-shader properties, memory properties, and API version; `supports_extension` checks name and minimum version.
|
||||||
- `get_physical_device_features` and `get_physical_device_properties` query the feature/property chains. Extension query results are consumed by the instance and logical-device setup helpers.
|
- `get_physical_device_features` and `get_physical_device_properties` query the feature/property chains. Extension query results are consumed by the instance and logical-device setup helpers.
|
||||||
- `DeviceExtensions` holds the debug-utils device loader and optional swapchain/mesh-shader loaders. `DeviceInner.enabled_extensions` records enabled extension names.
|
- `DeviceExtensions` holds the debug-utils device loader and optional swapchain/mesh-shader loaders. `DeviceInner.enabled_extensions` records enabled extension names.
|
||||||
- `AllocationStrategy` selects allocator-managed blocks or a dedicated allocation. `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations.
|
- `MemoryLocation` is the public allocator-independent memory preference; `AllocationDesc` combines it with a resource name and `AllocationStrategy` (`AllocatorManaged` or `Dedicated`). `GpuAllocation` wraps the private `vk_mem::Allocation` and retains memory properties needed by callers.
|
||||||
|
- `DeviceInner::allocate_for_buffer` / `allocate_for_image` obtain VMA allocations, and `bind_buffer_memory` / `bind_image_memory` bind them to Vulkan resources without exposing `vk_mem` types to upper layers. CPU-mapped locations require host-visible, host-coherent memory.
|
||||||
|
- `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations.
|
||||||
- `get_extensions` returns supported/enabled and unsupported requested extension sets; `get_layers` returns available requested layers or the supported/unsupported split.
|
- `get_extensions` returns supported/enabled and unsupported requested extension sets; `get_layers` returns available requested layers or the supported/unsupported split.
|
||||||
- `Queue` shares a `QueueInner` through `Arc`; `with_locked` serializes operations on the Vulkan queue. `QueueFamily` records index, count, flags, granularity, and timestamp bits.
|
- `Queue` shares a `QueueInner` through `Arc`; `with_locked` serializes operations on the Vulkan queue. `QueueFamily` records index, count, flags, granularity, and timestamp bits.
|
||||||
- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires presentation support only when a display handle is provided.
|
- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires presentation support only when a display handle is provided.
|
||||||
|
|
@ -94,7 +96,7 @@
|
||||||
- `Device::PartialEq` compares `Arc` identity, not raw Vulkan handles or physical-device identity.
|
- `Device::PartialEq` compares `Arc` identity, not raw Vulkan handles or physical-device identity.
|
||||||
- `Instance::get_adapter_surface_capabilities` receives the surface loader separately from the raw handle and returns format/mode data alongside generic capabilities.
|
- `Instance::get_adapter_surface_capabilities` receives the surface loader separately from the raw handle and returns format/mode data alongside generic capabilities.
|
||||||
- In debug builds `DebugName` stores its string; in non-debug builds its dereference returns a placeholder and does not retain the supplied name. `debug_name_object` assigns names through debug utils.
|
- In debug builds `DebugName` stores its string; in non-debug builds its dereference returns a placeholder and does not retain the supplied name. `debug_name_object` assigns names through debug utils.
|
||||||
- GPU allocation free is routed through `DeviceObject<GpuAllocation>` into the allocator mutex. Do not call allocator free on an allocation after ownership has moved or dropped.
|
- GPU allocation free is routed through `DeviceObject<GpuAllocation>` into the VMA allocator mutex with `free_memory`. Do not call allocator free on an allocation after ownership has moved or dropped.
|
||||||
- `ExternallyManagedObject<Device>` must destroy each owned handle once. The `vk::CommandBuffer` implementation is intentionally a no-op: its outer command-buffer wrapper frees it through its command pool. `vk::CommandPool` itself is really destroyed.
|
- `ExternallyManagedObject<Device>` must destroy each owned handle once. The `vk::CommandBuffer` implementation is intentionally a no-op: its outer command-buffer wrapper frees it through its command pool. `vk::CommandPool` itself is really destroyed.
|
||||||
- Device handle types are hand-written around `DeviceObject` with `DeviceOwned` implementations where needed; the old `define_device_owned_handle!` macro no longer exists.
|
- Device handle types are hand-written around `DeviceObject` with `DeviceOwned` implementations where needed; the old `define_device_owned_handle!` macro no longer exists.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ testing = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
ash-window = { workspace = true }
|
ash-window = { workspace = true }
|
||||||
gpu-allocator = { workspace = true }
|
vk-mem = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
raw-window-handle = { workspace = true }
|
raw-window-handle = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::{
|
||||||
collections::{BTreeSet, HashMap, HashSet},
|
collections::{BTreeSet, HashMap, HashSet},
|
||||||
ffi::CStr,
|
ffi::CStr,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
ptr::NonNull,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,7 +67,7 @@ impl From<String> for DebugName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use crate::{
|
use crate::{
|
||||||
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
Error, Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
||||||
device::asdf::traits::ExternallyManagedObject,
|
device::asdf::traits::ExternallyManagedObject,
|
||||||
pipeline_cache::PipelineCache,
|
pipeline_cache::PipelineCache,
|
||||||
queue::{DeviceQueueInfos, DeviceQueues, Queue},
|
queue::{DeviceQueueInfos, DeviceQueues, Queue},
|
||||||
|
|
@ -108,12 +109,89 @@ pub struct DeviceExtensions {
|
||||||
pub mesh_shader: Option<ext::mesh_shader::Device>,
|
pub mesh_shader: Option<ext::mesh_shader::Device>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type GpuAllocation = gpu_allocator::vulkan::Allocation;
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MemoryLocation {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
GpuOnly,
|
||||||
|
CpuToGpu,
|
||||||
|
GpuToCpu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SendSyncPtr(NonNull<u8>);
|
||||||
|
|
||||||
|
unsafe impl Send for SendSyncPtr {}
|
||||||
|
unsafe impl Sync for SendSyncPtr {}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for SendSyncPtr {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_tuple("SendSyncPtr").field(&self.0).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GpuAllocation {
|
||||||
|
raw: vk_mem::Allocation,
|
||||||
|
memory: vk::DeviceMemory,
|
||||||
|
offset: u64,
|
||||||
|
size: u64,
|
||||||
|
memory_type_index: u32,
|
||||||
|
mapped: Option<SendSyncPtr>,
|
||||||
|
dedicated_memory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for GpuAllocation {}
|
||||||
|
unsafe impl Sync for GpuAllocation {}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for GpuAllocation {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("GpuAllocation")
|
||||||
|
.field("memory", &self.memory)
|
||||||
|
.field("offset", &self.offset)
|
||||||
|
.field("size", &self.size)
|
||||||
|
.field("memory_type_index", &self.memory_type_index)
|
||||||
|
.field("mapped", &self.mapped)
|
||||||
|
.field("dedicated_memory", &self.dedicated_memory)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuAllocation {
|
||||||
|
pub fn memory(&self) -> vk::DeviceMemory {
|
||||||
|
self.memory
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset(&self) -> u64 {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn memory_type_index(&self) -> u32 {
|
||||||
|
self.memory_type_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dedicated(&self) -> bool {
|
||||||
|
self.dedicated_memory
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapped_slice(&self) -> Option<&[u8]> {
|
||||||
|
let mapped = self.mapped?;
|
||||||
|
Some(unsafe { core::slice::from_raw_parts(mapped.0.as_ptr(), self.size as usize) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mapped_slice_mut(&mut self) -> Option<&mut [u8]> {
|
||||||
|
let mapped = self.mapped?;
|
||||||
|
Some(unsafe { core::slice::from_raw_parts_mut(mapped.0.as_ptr(), self.size as usize) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum AllocationStrategy {
|
pub enum AllocationStrategy {
|
||||||
#[default]
|
#[default]
|
||||||
/// Let gpu_allocator manage the memory for this allocation, sub-allocating
|
/// Let the allocator manage the memory for this allocation, sub-allocating
|
||||||
/// from larger blocks as needed.
|
/// from larger blocks as needed.
|
||||||
AllocatorManaged,
|
AllocatorManaged,
|
||||||
/// Allocate a dedicated block of memory for this allocation. This is
|
/// Allocate a dedicated block of memory for this allocation. This is
|
||||||
|
|
@ -122,6 +200,12 @@ pub enum AllocationStrategy {
|
||||||
Dedicated,
|
Dedicated,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AllocationDesc<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub location: MemoryLocation,
|
||||||
|
pub strategy: AllocationStrategy,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Allocation {
|
pub enum Allocation {
|
||||||
Owned(DeviceObject<GpuAllocation>),
|
Owned(DeviceObject<GpuAllocation>),
|
||||||
|
|
@ -148,7 +232,7 @@ impl Allocation {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeviceInner {
|
pub struct DeviceInner {
|
||||||
pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>,
|
pub allocator: Mutex<vk_mem::Allocator>,
|
||||||
pub raw: ash::Device,
|
pub raw: ash::Device,
|
||||||
pub adapter: PhysicalDeviceInfo,
|
pub adapter: PhysicalDeviceInfo,
|
||||||
pub instance: Instance,
|
pub instance: Instance,
|
||||||
|
|
@ -438,25 +522,19 @@ impl PhysicalDeviceInfo {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let alloc2 =
|
let mut allocator_info =
|
||||||
gpu_allocator::vulkan::Allocator::new(&gpu_allocator::vulkan::AllocatorCreateDesc {
|
vk_mem::AllocatorCreateInfo::new(&instance.inner.raw, &device, self.pdev);
|
||||||
instance: instance.inner.raw.clone(),
|
allocator_info.preferred_large_heap_block_size = 64 * 1024 * 1024;
|
||||||
device: device.clone(),
|
allocator_info.vulkan_api_version = instance
|
||||||
physical_device: self.pdev,
|
.inner
|
||||||
debug_settings: Default::default(),
|
.api_version
|
||||||
buffer_device_address: false,
|
.min(self.properties.device_api_version);
|
||||||
allocation_sizes: {
|
let allocator = unsafe { vk_mem::Allocator::new(allocator_info) }?;
|
||||||
const MB: u64 = 1024 * 1024;
|
|
||||||
gpu_allocator::AllocationSizes::new(8 * MB, 64 * MB)
|
|
||||||
.with_max_host_memblock_size(256 * MB)
|
|
||||||
.with_max_device_memblock_size(256 * MB)
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let pipeline_cache = PipelineCache::new(&device, &self).unwrap();
|
let pipeline_cache = PipelineCache::new(&device, &self).unwrap();
|
||||||
let shared = Arc::new_cyclic(|inner: &Weak<DeviceInner>| DeviceInner {
|
let shared = Arc::new_cyclic(|inner: &Weak<DeviceInner>| DeviceInner {
|
||||||
raw: device.clone(),
|
raw: device.clone(),
|
||||||
alloc2: Mutex::new(alloc2),
|
allocator: Mutex::new(allocator),
|
||||||
instance: instance.clone(),
|
instance: instance.clone(),
|
||||||
adapter: self,
|
adapter: self,
|
||||||
queues: device_queues,
|
queues: device_queues,
|
||||||
|
|
@ -579,6 +657,126 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceInner {
|
impl DeviceInner {
|
||||||
|
fn allocation_create_info(
|
||||||
|
&self,
|
||||||
|
desc: &AllocationDesc<'_>,
|
||||||
|
) -> Result<vk_mem::AllocationCreateInfo> {
|
||||||
|
use vk_mem::{AllocationCreateFlags, AllocationCreateInfo, MemoryUsage};
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let mut info = AllocationCreateInfo {
|
||||||
|
usage: match desc.location {
|
||||||
|
MemoryLocation::Unknown => MemoryUsage::Unknown,
|
||||||
|
MemoryLocation::GpuOnly => MemoryUsage::GpuOnly,
|
||||||
|
MemoryLocation::CpuToGpu => MemoryUsage::CpuToGpu,
|
||||||
|
MemoryLocation::GpuToCpu => MemoryUsage::GpuToCpu,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
desc.location,
|
||||||
|
MemoryLocation::CpuToGpu | MemoryLocation::GpuToCpu
|
||||||
|
) {
|
||||||
|
let memory_properties = &self.adapter.properties.memory_properties;
|
||||||
|
let coherent_host_types = memory_properties
|
||||||
|
.memory_types
|
||||||
|
.iter()
|
||||||
|
.take(memory_properties.memory_type_count as usize)
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, memory_type)| {
|
||||||
|
memory_type.property_flags.contains(
|
||||||
|
vk::MemoryPropertyFlags::HOST_VISIBLE
|
||||||
|
| vk::MemoryPropertyFlags::HOST_COHERENT,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.fold(0_u32, |bits, (index, _)| bits | (1 << index));
|
||||||
|
if coherent_host_types == 0 {
|
||||||
|
return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT.into());
|
||||||
|
}
|
||||||
|
info.memory_type_bits = coherent_host_types;
|
||||||
|
info.flags |= AllocationCreateFlags::MAPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
let api_version = self
|
||||||
|
.instance
|
||||||
|
.inner
|
||||||
|
.api_version
|
||||||
|
.min(self.adapter.properties.device_api_version);
|
||||||
|
if desc.strategy == AllocationStrategy::Dedicated && api_version < vk::API_VERSION_1_1 {
|
||||||
|
return Err(Error::DedicatedAllocationRequiresApi11);
|
||||||
|
}
|
||||||
|
if desc.strategy == AllocationStrategy::Dedicated {
|
||||||
|
info.flags |= AllocationCreateFlags::DEDICATED_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_for_buffer(
|
||||||
|
&self,
|
||||||
|
buffer: vk::Buffer,
|
||||||
|
desc: AllocationDesc<'_>,
|
||||||
|
) -> Result<GpuAllocation> {
|
||||||
|
use vk_mem::Alloc as _;
|
||||||
|
|
||||||
|
let create_info = self.allocation_create_info(&desc)?;
|
||||||
|
let allocator = self.allocator.lock();
|
||||||
|
let raw = unsafe { allocator.allocate_memory_for_buffer(buffer, &create_info) }?;
|
||||||
|
let allocation_info = allocator.get_allocation_info2(&raw);
|
||||||
|
let dedicated_memory = allocation_info.dedicated_memory;
|
||||||
|
let allocation_info = allocation_info.allocation_info;
|
||||||
|
let mapped = NonNull::new(allocation_info.mapped_data.cast::<u8>()).map(SendSyncPtr);
|
||||||
|
Ok(GpuAllocation {
|
||||||
|
raw,
|
||||||
|
memory: allocation_info.device_memory,
|
||||||
|
offset: allocation_info.offset,
|
||||||
|
size: allocation_info.size,
|
||||||
|
memory_type_index: allocation_info.memory_type,
|
||||||
|
mapped,
|
||||||
|
dedicated_memory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_for_image(
|
||||||
|
&self,
|
||||||
|
image: vk::Image,
|
||||||
|
desc: AllocationDesc<'_>,
|
||||||
|
) -> Result<GpuAllocation> {
|
||||||
|
use vk_mem::Alloc as _;
|
||||||
|
|
||||||
|
let create_info = self.allocation_create_info(&desc)?;
|
||||||
|
let allocator = self.allocator.lock();
|
||||||
|
let raw = unsafe { allocator.allocate_memory_for_image(image, &create_info) }?;
|
||||||
|
let allocation_info = allocator.get_allocation_info2(&raw);
|
||||||
|
let dedicated_memory = allocation_info.dedicated_memory;
|
||||||
|
let allocation_info = allocation_info.allocation_info;
|
||||||
|
let mapped = NonNull::new(allocation_info.mapped_data.cast::<u8>()).map(SendSyncPtr);
|
||||||
|
Ok(GpuAllocation {
|
||||||
|
raw,
|
||||||
|
memory: allocation_info.device_memory,
|
||||||
|
offset: allocation_info.offset,
|
||||||
|
size: allocation_info.size,
|
||||||
|
memory_type_index: allocation_info.memory_type,
|
||||||
|
mapped,
|
||||||
|
dedicated_memory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_buffer_memory(&self, alloc: &GpuAllocation, buffer: vk::Buffer) -> Result<()> {
|
||||||
|
unsafe {
|
||||||
|
self.allocator
|
||||||
|
.lock()
|
||||||
|
.bind_buffer_memory(&alloc.raw, buffer)?
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_image_memory(&self, alloc: &GpuAllocation, image: vk::Image) -> Result<()> {
|
||||||
|
unsafe { self.allocator.lock().bind_image_memory(&alloc.raw, image)? };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sync_threadpool(&self) -> &sync::SyncThreadpool {
|
pub fn sync_threadpool(&self) -> &sync::SyncThreadpool {
|
||||||
&self.sync_threadpool
|
&self.sync_threadpool
|
||||||
}
|
}
|
||||||
|
|
@ -1072,8 +1270,8 @@ pub mod asdf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl traits::ExternallyManagedObject<Device> for GpuAllocation {
|
impl traits::ExternallyManagedObject<Device> for GpuAllocation {
|
||||||
unsafe fn destroy(self, device: &Device) {
|
unsafe fn destroy(mut self, device: &Device) {
|
||||||
_ = device.alloc2.lock().free(self);
|
unsafe { device.allocator.lock().free_memory(&mut self.raw) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1155,4 +1353,11 @@ pub mod asdf {
|
||||||
vk::FALSE
|
vk::FALSE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gpu_allocation_is_send_sync() {
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
|
||||||
|
assert_send_sync::<super::GpuAllocation>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ pub struct InstanceInner {
|
||||||
// it drops before `raw` during instance teardown.
|
// it drops before `raw` during instance teardown.
|
||||||
pub _debug_utils: DebugUtils,
|
pub _debug_utils: DebugUtils,
|
||||||
pub raw: ash::Instance,
|
pub raw: ash::Instance,
|
||||||
|
pub api_version: u32,
|
||||||
pub extensions: InstanceExtensions,
|
pub extensions: InstanceExtensions,
|
||||||
pub entry: Entry,
|
pub entry: Entry,
|
||||||
}
|
}
|
||||||
|
|
@ -123,16 +124,14 @@ impl Instance {
|
||||||
unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0);
|
unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0);
|
||||||
|
|
||||||
let app_info = vk::ApplicationInfo::default()
|
let app_info = vk::ApplicationInfo::default()
|
||||||
.api_version(if version < vk::API_VERSION_1_1 {
|
// ash/VMA support Vulkan 1.3, and the application must not request a
|
||||||
vk::API_VERSION_1_0
|
// version newer than the loader supports.
|
||||||
} else {
|
.api_version(version.min(vk::API_VERSION_1_3))
|
||||||
// ash doesn't support 1.4 yet
|
|
||||||
vk::API_VERSION_1_3
|
|
||||||
})
|
|
||||||
.application_name(&app_name)
|
.application_name(&app_name)
|
||||||
.application_version(desc.app_version)
|
.application_version(desc.app_version)
|
||||||
.engine_name(c"Bevy Engine")
|
.engine_name(c"Bevy Engine")
|
||||||
.engine_version(vk::make_api_version(0, 0, 1, 0));
|
.engine_version(vk::make_api_version(0, 0, 1, 0));
|
||||||
|
let api_version = app_info.api_version;
|
||||||
|
|
||||||
let mut validation_info =
|
let mut validation_info =
|
||||||
vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
|
vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
|
||||||
|
|
@ -228,6 +227,7 @@ impl Instance {
|
||||||
let instance = Arc::new(InstanceInner {
|
let instance = Arc::new(InstanceInner {
|
||||||
_debug_utils: debug_utils,
|
_debug_utils: debug_utils,
|
||||||
raw: instance,
|
raw: instance,
|
||||||
|
api_version,
|
||||||
extensions,
|
extensions,
|
||||||
entry,
|
entry,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,12 @@ pub enum Error {
|
||||||
Utf8Error(#[from] core::str::Utf8Error),
|
Utf8Error(#[from] core::str::Utf8Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NulError(#[from] std::ffi::NulError),
|
NulError(#[from] std::ffi::NulError),
|
||||||
#[error(transparent)]
|
|
||||||
GpuAllocatorError(#[from] gpu_allocator::AllocationError),
|
|
||||||
#[error("No Physical Device found.")]
|
#[error("No Physical Device found.")]
|
||||||
NoPhysicalDevice,
|
NoPhysicalDevice,
|
||||||
|
#[error(
|
||||||
|
"Dedicated allocation requires Vulkan 1.1 or both VK_KHR_dedicated_allocation and VK_KHR_get_memory_requirements2 extensions."
|
||||||
|
)]
|
||||||
|
DedicatedAllocationRequiresApi11,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error(
|
#[error(
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ unsafe extern "system" fn capture_callback(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::device::{AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instance_and_device_creation() {
|
fn instance_and_device_creation() {
|
||||||
|
|
@ -305,30 +306,23 @@ mod tests {
|
||||||
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
|
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||||
let buffer = unsafe { device.dev().create_buffer(&info, None) }.expect("create buffer");
|
let buffer = unsafe { device.dev().create_buffer(&info, None) }.expect("create buffer");
|
||||||
let requirements = unsafe { device.dev().get_buffer_memory_requirements(buffer) };
|
let buffer = DeviceObject::new(device.clone(), buffer);
|
||||||
let allocation = device
|
let allocation = device
|
||||||
.alloc2
|
.allocate_for_buffer(
|
||||||
.lock()
|
*buffer,
|
||||||
.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
|
AllocationDesc {
|
||||||
name: "renderer_device validation smoke buffer",
|
name: "renderer_device validation smoke buffer",
|
||||||
requirements,
|
location: MemoryLocation::CpuToGpu,
|
||||||
location: gpu_allocator::MemoryLocation::CpuToGpu,
|
strategy: AllocationStrategy::AllocatorManaged,
|
||||||
linear: true,
|
},
|
||||||
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
|
)
|
||||||
})
|
|
||||||
.expect("allocate buffer memory");
|
.expect("allocate buffer memory");
|
||||||
unsafe {
|
let allocation = DeviceObject::new(device.clone(), allocation);
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
|
|
||||||
.expect("bind buffer memory");
|
|
||||||
device.dev().destroy_buffer(buffer, None);
|
|
||||||
}
|
|
||||||
device
|
device
|
||||||
.alloc2
|
.bind_buffer_memory(&allocation, *buffer)
|
||||||
.lock()
|
.expect("bind buffer memory");
|
||||||
.free(allocation)
|
drop(buffer);
|
||||||
.expect("free buffer memory");
|
drop(allocation);
|
||||||
test.assert_no_validation_errors();
|
test.assert_no_validation_errors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,24 +338,21 @@ mod tests {
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||||
let buffer = unsafe { device.dev().create_buffer(&buffer_info, None) }
|
let buffer = unsafe { device.dev().create_buffer(&buffer_info, None) }
|
||||||
.expect("create validation probe buffer");
|
.expect("create validation probe buffer");
|
||||||
let requirements = unsafe { device.dev().get_buffer_memory_requirements(buffer) };
|
let buffer = DeviceObject::new(device.clone(), buffer);
|
||||||
let allocation = device
|
let allocation = device
|
||||||
.alloc2
|
.allocate_for_buffer(
|
||||||
.lock()
|
*buffer,
|
||||||
.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
|
AllocationDesc {
|
||||||
name: "renderer_device validation probe buffer",
|
name: "renderer_device validation probe buffer",
|
||||||
requirements,
|
location: MemoryLocation::CpuToGpu,
|
||||||
location: gpu_allocator::MemoryLocation::CpuToGpu,
|
strategy: AllocationStrategy::AllocatorManaged,
|
||||||
linear: true,
|
},
|
||||||
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
|
)
|
||||||
})
|
|
||||||
.expect("allocate validation probe buffer");
|
.expect("allocate validation probe buffer");
|
||||||
unsafe {
|
let allocation = DeviceObject::new(device.clone(), allocation);
|
||||||
device
|
device
|
||||||
.dev()
|
.bind_buffer_memory(&allocation, *buffer)
|
||||||
.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
|
.expect("bind validation probe buffer");
|
||||||
.expect("bind validation probe buffer");
|
|
||||||
}
|
|
||||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
let pool_info = vk::CommandPoolCreateInfo::default()
|
||||||
.queue_family_index(device.queues.graphics().family.index.index());
|
.queue_family_index(device.queues.graphics().family.index.index());
|
||||||
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
|
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
|
||||||
|
|
@ -380,19 +371,15 @@ mod tests {
|
||||||
// The size must be a multiple of four. The invalid command is never submitted.
|
// The size must be a multiple of four. The invalid command is never submitted.
|
||||||
device
|
device
|
||||||
.dev()
|
.dev()
|
||||||
.cmd_fill_buffer(command_buffer, buffer, 0, 3, 0);
|
.cmd_fill_buffer(command_buffer, *buffer, 0, 3, 0);
|
||||||
device
|
device
|
||||||
.dev()
|
.dev()
|
||||||
.end_command_buffer(command_buffer)
|
.end_command_buffer(command_buffer)
|
||||||
.expect("end command buffer");
|
.expect("end command buffer");
|
||||||
device.dev().destroy_command_pool(pool, None);
|
device.dev().destroy_command_pool(pool, None);
|
||||||
device.dev().destroy_buffer(buffer, None);
|
|
||||||
}
|
}
|
||||||
device
|
drop(buffer);
|
||||||
.alloc2
|
drop(allocation);
|
||||||
.lock()
|
|
||||||
.free(allocation)
|
|
||||||
.expect("free probe allocation");
|
|
||||||
|
|
||||||
let messages = test.take_messages();
|
let messages = test.take_messages();
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash.workspace = true
|
ash.workspace = true
|
||||||
gpu-allocator.workspace = true
|
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
renderer_resources = { path = "../renderer_resources" }
|
renderer_resources = { path = "../renderer_resources" }
|
||||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
use renderer_device::device::MemoryLocation;
|
||||||
use renderer_resources::{
|
use renderer_resources::{
|
||||||
buffers::Buffer as BufferResource,
|
buffers::Buffer as BufferResource,
|
||||||
images::{Image as ImageResource, ImageSubresourceExt},
|
images::{Image as ImageResource, ImageSubresourceExt},
|
||||||
|
|
@ -13,7 +14,7 @@ use renderer_resources::{
|
||||||
pub struct GraphBufferDesc {
|
pub struct GraphBufferDesc {
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub name: Option<Cow<'static, str>>,
|
pub name: Option<Cow<'static, str>>,
|
||||||
pub mem_location: gpu_allocator::MemoryLocation,
|
pub mem_location: MemoryLocation,
|
||||||
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
||||||
pub extra_usage: vk::BufferUsageFlags,
|
pub extra_usage: vk::BufferUsageFlags,
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +25,7 @@ impl Default for GraphBufferDesc {
|
||||||
size: 0,
|
size: 0,
|
||||||
name: None,
|
name: None,
|
||||||
// Transient graph resources default to device-local memory.
|
// Transient graph resources default to device-local memory.
|
||||||
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
mem_location: MemoryLocation::GpuOnly,
|
||||||
extra_usage: vk::BufferUsageFlags::empty(),
|
extra_usage: vk::BufferUsageFlags::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +44,7 @@ pub struct GraphTextureDesc {
|
||||||
pub tiling: vk::ImageTiling,
|
pub tiling: vk::ImageTiling,
|
||||||
pub layout: vk::ImageLayout,
|
pub layout: vk::ImageLayout,
|
||||||
pub name: Option<Cow<'static, str>>,
|
pub name: Option<Cow<'static, str>>,
|
||||||
pub mem_location: gpu_allocator::MemoryLocation,
|
pub mem_location: MemoryLocation,
|
||||||
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
||||||
pub extra_usage: vk::ImageUsageFlags,
|
pub extra_usage: vk::ImageUsageFlags,
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +67,7 @@ impl Default for GraphTextureDesc {
|
||||||
tiling: vk::ImageTiling::OPTIMAL,
|
tiling: vk::ImageTiling::OPTIMAL,
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
name: None,
|
name: None,
|
||||||
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
mem_location: MemoryLocation::GpuOnly,
|
||||||
extra_usage: vk::ImageUsageFlags::empty(),
|
extra_usage: vk::ImageUsageFlags::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use renderer_device::{device::QueueFlags, testing::TestVulkan};
|
use renderer_device::{
|
||||||
|
device::{MemoryLocation, QueueFlags},
|
||||||
|
testing::TestVulkan,
|
||||||
|
};
|
||||||
use renderer_rendergraph::{
|
use renderer_rendergraph::{
|
||||||
RenderGraph,
|
RenderGraph,
|
||||||
commands::CopyBuffer,
|
commands::CopyBuffer,
|
||||||
|
|
@ -28,7 +31,7 @@ fn imported_buffer_copy_records_under_validation() {
|
||||||
size: 256,
|
size: 256,
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
mem_location: gpu_allocator::MemoryLocation::CpuToGpu,
|
mem_location: MemoryLocation::CpuToGpu,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
|
|
||||||
## Key types and API
|
## Key types and API
|
||||||
- These modules are public from `renderer_resources`; the crate root re-exports the core device types and `device` module from `renderer_device` for resource construction.
|
- These modules are public from `renderer_resources`; the crate root re-exports the core device types and `device` module from `renderer_device` for resource construction.
|
||||||
- `BufferDesc` configures Vulkan buffer flags, size, usage, queue families, memory location, allocation strategy, and optional debug name. `Buffer::new` allocates/binds memory; `map` and `map_mut` return mapped slices when available.
|
- `BufferDesc` configures Vulkan buffer flags, size, usage, queue families, `renderer_device::device::MemoryLocation`, allocation strategy, and optional debug name. `Buffer::new` requests a VMA allocation through `DeviceInner::allocate_for_buffer` and binds it through `bind_buffer_memory`; `map` and `map_mut` return mapped slices when available.
|
||||||
- `Buffer::raw` returns the Vulkan handle and `len` returns the configured size. Mapping is bounded to the descriptor size, not the allocator block size.
|
- `Buffer::raw` returns the Vulkan handle and `len` returns the configured size. Mapping is bounded to the descriptor size, not the VMA allocation block size. CPU-mapped locations select host-visible, host-coherent memory.
|
||||||
- `ImageDesc` configures image type, format, extent, layers/mips, samples, tiling, usage, initial layout, queue sharing, and allocation.
|
- `ImageDesc` configures image type, format, extent, layers/mips, samples, tiling, usage, initial layout, queue sharing, and allocation strategy/location. `Image::new` uses the device's VMA allocation and image-binding helpers.
|
||||||
- `Image::new` creates an allocated image. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
|
- `Image::new` creates an allocated image. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
|
||||||
- `Image::from_swapchain_raw(image, device, format, extent, usage)` builds an image wrapper for an image owned by a swapchain; renderer calls it from `Swapchain::acquire_image`.
|
- `Image::from_swapchain_raw(image, device, format, extent, usage)` builds an image wrapper for an image owned by a swapchain; renderer calls it from `Swapchain::acquire_image`.
|
||||||
- `ImageViewDesc` describes a view's type, format, component mapping, aspect, mip range, and layer range. `Image::create_view` validates compatibility and returns `ManuallyDrop<ImageView>`.
|
- `ImageViewDesc` describes a view's type, format, component mapping, aspect, mip range, and layer range. `Image::create_view` validates compatibility and returns `ManuallyDrop<ImageView>`.
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ testing = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
gpu-allocator = { workspace = true }
|
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
bytemuck = { workspace = true }
|
bytemuck = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use gpu_allocator::vulkan::AllocationScheme;
|
|
||||||
use renderer_device::queue::QueueFamilyIndices;
|
use renderer_device::queue::QueueFamilyIndices;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Device,
|
Device,
|
||||||
device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
|
device::{
|
||||||
|
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -17,7 +18,7 @@ pub struct BufferDesc {
|
||||||
pub usage: vk::BufferUsageFlags,
|
pub usage: vk::BufferUsageFlags,
|
||||||
pub queue_families: QueueFlags,
|
pub queue_families: QueueFlags,
|
||||||
|
|
||||||
pub mem_location: gpu_allocator::MemoryLocation,
|
pub mem_location: MemoryLocation,
|
||||||
pub alloc_scheme: AllocationStrategy,
|
pub alloc_scheme: AllocationStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ impl Default for BufferDesc {
|
||||||
size: Default::default(),
|
size: Default::default(),
|
||||||
usage: Default::default(),
|
usage: Default::default(),
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
mem_location: gpu_allocator::MemoryLocation::Unknown,
|
mem_location: MemoryLocation::Unknown,
|
||||||
alloc_scheme: AllocationStrategy::default(),
|
alloc_scheme: AllocationStrategy::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,46 +103,30 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
||||||
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
|
let buffer = Self::new_raw(device.clone(), &desc)?;
|
||||||
|
let alloc = device.allocate_for_buffer(
|
||||||
|
*buffer,
|
||||||
|
AllocationDesc {
|
||||||
|
name: desc.name.as_deref().unwrap_or_default(),
|
||||||
|
location: desc.mem_location,
|
||||||
|
strategy: desc.alloc_scheme,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
let alloc =
|
let buffer = Self {
|
||||||
device
|
buffer: BufferHandle::Owned(buffer),
|
||||||
.alloc2
|
|
||||||
.lock()
|
|
||||||
.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
|
|
||||||
name: desc.name.as_deref().unwrap_or_default(),
|
|
||||||
requirements,
|
|
||||||
location: desc.mem_location,
|
|
||||||
linear: true,
|
|
||||||
allocation_scheme: match desc.alloc_scheme {
|
|
||||||
AllocationStrategy::AllocatorManaged => {
|
|
||||||
AllocationScheme::GpuAllocatorManaged
|
|
||||||
}
|
|
||||||
AllocationStrategy::Dedicated => AllocationScheme::DedicatedBuffer(buffer),
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.raw
|
|
||||||
.bind_buffer_memory(buffer, alloc.memory(), alloc.offset())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
buffer: BufferHandle::Owned(DeviceObject::new_debug_named(
|
|
||||||
device.clone(),
|
|
||||||
buffer,
|
|
||||||
desc.name.clone(),
|
|
||||||
)),
|
|
||||||
desc,
|
desc,
|
||||||
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
|
alloc: Allocation::Owned(DeviceObject::new(device.clone(), alloc)),
|
||||||
})
|
};
|
||||||
|
device.bind_buffer_memory(
|
||||||
|
buffer.alloc.allocation().expect("owned buffer allocation"),
|
||||||
|
buffer.raw(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_raw(
|
fn new_raw(device: Device, desc: &BufferDesc) -> crate::Result<DeviceObject<vk::Buffer>> {
|
||||||
device: Device,
|
|
||||||
desc: &BufferDesc,
|
|
||||||
) -> crate::Result<(vk::Buffer, vk::MemoryRequirements)> {
|
|
||||||
let queue_families = device.queues.family_indices(desc.queue_families);
|
let queue_families = device.queues.family_indices(desc.queue_families);
|
||||||
|
|
||||||
let sharing_mode = if queue_families.len() > 1 {
|
let sharing_mode = if queue_families.len() > 1 {
|
||||||
|
|
@ -156,9 +141,11 @@ impl Buffer {
|
||||||
.sharing_mode(sharing_mode);
|
.sharing_mode(sharing_mode);
|
||||||
|
|
||||||
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };
|
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };
|
||||||
let mem_reqs = unsafe { device.dev().get_buffer_memory_requirements(buffer) };
|
Ok(DeviceObject::new_debug_named(
|
||||||
|
device,
|
||||||
Ok((buffer, mem_reqs))
|
buffer,
|
||||||
|
desc.name.clone(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map(&mut self) -> Option<&[u8]> {
|
pub fn map(&mut self) -> Option<&[u8]> {
|
||||||
|
|
@ -210,6 +197,112 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod allocation_validation_tests {
|
||||||
|
use ash::vk;
|
||||||
|
use renderer_device::testing::TestVulkan;
|
||||||
|
|
||||||
|
use super::{Buffer, BufferDesc};
|
||||||
|
use crate::{
|
||||||
|
device::MemoryLocation,
|
||||||
|
images::{Image, ImageDesc},
|
||||||
|
};
|
||||||
|
use renderer_device::device::AllocationStrategy;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vma_mapping_and_dedicated_resources_under_validation() {
|
||||||
|
let test = TestVulkan::expect();
|
||||||
|
let device = test.device().clone();
|
||||||
|
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
assert_send_sync::<Buffer>();
|
||||||
|
assert_send_sync::<Image>();
|
||||||
|
|
||||||
|
let mut mapped_buffer = Buffer::new(
|
||||||
|
device.clone(),
|
||||||
|
BufferDesc {
|
||||||
|
size: 64,
|
||||||
|
usage: vk::BufferUsageFlags::TRANSFER_SRC,
|
||||||
|
mem_location: MemoryLocation::CpuToGpu,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create CpuToGpu buffer");
|
||||||
|
let allocation = mapped_buffer
|
||||||
|
.alloc
|
||||||
|
.allocation()
|
||||||
|
.expect("CpuToGpu buffer allocation");
|
||||||
|
let memory_type = test
|
||||||
|
.physical_device()
|
||||||
|
.properties
|
||||||
|
.memory_properties
|
||||||
|
.memory_types[allocation.memory_type_index() as usize];
|
||||||
|
assert!(memory_type.property_flags.contains(
|
||||||
|
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
|
||||||
|
));
|
||||||
|
|
||||||
|
let expected = (0..64).map(|byte| byte as u8).collect::<Vec<_>>();
|
||||||
|
mapped_buffer
|
||||||
|
.map_mut()
|
||||||
|
.expect("CpuToGpu buffer is mapped")
|
||||||
|
.copy_from_slice(&expected);
|
||||||
|
assert_eq!(
|
||||||
|
mapped_buffer.map().expect("CpuToGpu buffer remains mapped"),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
|
||||||
|
let dedicated_buffer = Buffer::new(
|
||||||
|
device.clone(),
|
||||||
|
BufferDesc {
|
||||||
|
size: 256,
|
||||||
|
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
||||||
|
alloc_scheme: AllocationStrategy::Dedicated,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create dedicated buffer");
|
||||||
|
assert!(
|
||||||
|
dedicated_buffer
|
||||||
|
.alloc
|
||||||
|
.allocation()
|
||||||
|
.expect("dedicated buffer allocation")
|
||||||
|
.is_dedicated(),
|
||||||
|
"VMA should allocate the dedicated buffer from its own memory block"
|
||||||
|
);
|
||||||
|
|
||||||
|
let dedicated_image = Image::new(
|
||||||
|
device,
|
||||||
|
ImageDesc {
|
||||||
|
format: vk::Format::R8G8B8A8_UNORM,
|
||||||
|
extent: vk::Extent3D {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
usage: vk::ImageUsageFlags::SAMPLED,
|
||||||
|
alloc_scheme: AllocationStrategy::Dedicated,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create dedicated image");
|
||||||
|
assert!(
|
||||||
|
dedicated_image
|
||||||
|
.allocation()
|
||||||
|
.expect("dedicated image allocation")
|
||||||
|
.allocation()
|
||||||
|
.expect("dedicated image VMA allocation")
|
||||||
|
.is_dedicated(),
|
||||||
|
"VMA should allocate the dedicated image from its own memory block"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(dedicated_image);
|
||||||
|
drop(dedicated_buffer);
|
||||||
|
drop(mapped_buffer);
|
||||||
|
test.device().wait_idle().expect("wait for device idle");
|
||||||
|
test.assert_no_validation_errors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte range selected by a buffer view.
|
/// Byte range selected by a buffer view.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct BufferRange {
|
pub struct BufferRange {
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,15 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
|
device::{
|
||||||
|
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
|
||||||
|
},
|
||||||
util::WeakVec,
|
util::WeakVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Device;
|
use super::Device;
|
||||||
use ash::vk::{self, Handle};
|
use ash::vk::{self, Handle};
|
||||||
use ash_formats::FormatExt;
|
use ash_formats::FormatExt;
|
||||||
use gpu_allocator::vulkan::{AllocationCreateDesc, AllocationScheme};
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use renderer_device::queue::QueueFamilyIndices;
|
use renderer_device::queue::QueueFamilyIndices;
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ pub struct ImageDesc {
|
||||||
pub queue_families: QueueFlags,
|
pub queue_families: QueueFlags,
|
||||||
pub layout: vk::ImageLayout,
|
pub layout: vk::ImageLayout,
|
||||||
|
|
||||||
pub mem_location: gpu_allocator::MemoryLocation,
|
pub mem_location: MemoryLocation,
|
||||||
pub alloc_scheme: AllocationStrategy,
|
pub alloc_scheme: AllocationStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +124,7 @@ impl Default for ImageDesc {
|
||||||
usage: Default::default(),
|
usage: Default::default(),
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
mem_location: gpu_allocator::MemoryLocation::Unknown,
|
mem_location: MemoryLocation::Unknown,
|
||||||
alloc_scheme: AllocationStrategy::AllocatorManaged,
|
alloc_scheme: AllocationStrategy::AllocatorManaged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,17 +188,15 @@ pub struct Image {
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
pub fn new(device: Device, desc: ImageDesc) -> crate::Result<Self> {
|
pub fn new(device: Device, desc: ImageDesc) -> crate::Result<Self> {
|
||||||
let (image, requirements) = Self::new_raw(device.clone(), &desc)?;
|
let image = Self::new_raw(device.clone(), &desc)?;
|
||||||
let alloc = device.alloc2.lock().allocate(&AllocationCreateDesc {
|
let alloc = device.allocate_for_image(
|
||||||
name: desc.name.as_deref().unwrap_or(""),
|
*image,
|
||||||
requirements,
|
AllocationDesc {
|
||||||
location: desc.mem_location,
|
name: desc.name.as_deref().unwrap_or(""),
|
||||||
linear: desc.tiling == vk::ImageTiling::LINEAR,
|
location: desc.mem_location,
|
||||||
allocation_scheme: match desc.alloc_scheme {
|
strategy: desc.alloc_scheme,
|
||||||
AllocationStrategy::AllocatorManaged => AllocationScheme::GpuAllocatorManaged,
|
|
||||||
AllocationStrategy::Dedicated => AllocationScheme::DedicatedImage(image),
|
|
||||||
},
|
},
|
||||||
})?;
|
)?;
|
||||||
|
|
||||||
Self::new_with_allocation_unchecked(
|
Self::new_with_allocation_unchecked(
|
||||||
device.clone(),
|
device.clone(),
|
||||||
|
|
@ -209,27 +208,24 @@ impl Image {
|
||||||
|
|
||||||
fn new_with_allocation_unchecked(
|
fn new_with_allocation_unchecked(
|
||||||
device: Device,
|
device: Device,
|
||||||
image: vk::Image,
|
image: DeviceObject<vk::Image>,
|
||||||
allocation: Allocation,
|
allocation: Allocation,
|
||||||
desc: ImageDesc,
|
desc: ImageDesc,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
// bind memory
|
let image = Self {
|
||||||
if let Some(alloc) = allocation.allocation() {
|
image: ImageInner::Allocated(image, allocation),
|
||||||
unsafe {
|
desc,
|
||||||
device
|
views: Default::default(),
|
||||||
.raw
|
};
|
||||||
.bind_image_memory(image, alloc.memory(), alloc.offset())?;
|
|
||||||
|
// Bind after ownership is established so failures destroy the image before freeing memory.
|
||||||
|
if let ImageInner::Allocated(raw_image, allocation) = &image.image {
|
||||||
|
if let Some(alloc) = allocation.allocation() {
|
||||||
|
device.bind_image_memory(alloc, **raw_image)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(image)
|
||||||
image: ImageInner::Allocated(
|
|
||||||
DeviceObject::new_debug_named(device, image, desc.name.clone()),
|
|
||||||
allocation,
|
|
||||||
),
|
|
||||||
desc,
|
|
||||||
views: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_allocation(
|
pub fn new_with_allocation(
|
||||||
|
|
@ -237,7 +233,8 @@ impl Image {
|
||||||
allocation: Allocation,
|
allocation: Allocation,
|
||||||
desc: ImageDesc,
|
desc: ImageDesc,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let (image, requirements) = Self::new_raw(device.clone(), &desc)?;
|
let image = Self::new_raw(device.clone(), &desc)?;
|
||||||
|
let requirements = unsafe { device.dev().get_image_memory_requirements(*image) };
|
||||||
|
|
||||||
// validate allocation
|
// validate allocation
|
||||||
let alloc_size = allocation
|
let alloc_size = allocation
|
||||||
|
|
@ -286,7 +283,7 @@ impl Image {
|
||||||
usage,
|
usage,
|
||||||
queue_families: QueueFlags::PRESENT,
|
queue_families: QueueFlags::PRESENT,
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
mem_location: MemoryLocation::GpuOnly,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
views: Default::default(),
|
views: Default::default(),
|
||||||
|
|
@ -307,10 +304,7 @@ impl Image {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_raw(
|
fn new_raw(device: Device, desc: &ImageDesc) -> crate::Result<DeviceObject<vk::Image>> {
|
||||||
device: Device,
|
|
||||||
desc: &ImageDesc,
|
|
||||||
) -> crate::Result<(vk::Image, vk::MemoryRequirements)> {
|
|
||||||
tracing::trace!("new image with desc={desc:?}");
|
tracing::trace!("new image with desc={desc:?}");
|
||||||
let ImageDesc {
|
let ImageDesc {
|
||||||
flags,
|
flags,
|
||||||
|
|
@ -372,9 +366,11 @@ impl Image {
|
||||||
|
|
||||||
let image = unsafe { device.raw.create_image(&info, None)? };
|
let image = unsafe { device.raw.create_image(&info, None)? };
|
||||||
|
|
||||||
let requirements = unsafe { device.raw.get_image_memory_requirements(image) };
|
Ok(DeviceObject::new_debug_named(
|
||||||
|
device,
|
||||||
Ok((image, requirements))
|
image,
|
||||||
|
desc.name.clone(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn raw(&self) -> vk::Image {
|
pub fn raw(&self) -> vk::Image {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use renderer_device::{device::QueueFlags, testing::TestVulkan};
|
use renderer_device::{
|
||||||
|
device::{MemoryLocation, QueueFlags},
|
||||||
|
testing::TestVulkan,
|
||||||
|
};
|
||||||
use renderer_resources::{
|
use renderer_resources::{
|
||||||
buffers::{Buffer, BufferDesc},
|
buffers::{Buffer, BufferDesc},
|
||||||
pipeline::{
|
pipeline::{
|
||||||
|
|
@ -23,7 +26,7 @@ fn managed_set_write_and_pool_reset_under_validation() {
|
||||||
size: 256,
|
size: 256,
|
||||||
usage: vk::BufferUsageFlags::UNIFORM_BUFFER,
|
usage: vk::BufferUsageFlags::UNIFORM_BUFFER,
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
mem_location: gpu_allocator::MemoryLocation::CpuToGpu,
|
mem_location: MemoryLocation::CpuToGpu,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
326
design-plans/gpu-allocator-to-vma-migration.md
Normal file
326
design-plans/gpu-allocator-to-vma-migration.md
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
# 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`; use `vk-mem`/VMA; preserve the dependency
|
||||||
|
direction `bevy_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_resources` must not depend on `renderer`.
|
||||||
|
- `renderer_device` owns device/allocator state.
|
||||||
|
- `renderer_resources`/`renderer` should not expose `vk_mem` types.
|
||||||
|
- The `testing` feature gates real-device tests.
|
||||||
|
- No per-frame allocation is acceptable on current paths.
|
||||||
|
- `renderer_rendergraph` is also a workspace member and directly uses
|
||||||
|
`gpu_allocator::MemoryLocation`, so it must be included (it is not mentioned in
|
||||||
|
the workspace guide).
|
||||||
|
|
||||||
|
### Current architecture
|
||||||
|
|
||||||
|
- Deps: root `Cargo.toml:45` git `gpu-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-3435` is the git package;
|
||||||
|
`Cargo.lock:3409-3420` is the registry copy pulled by `wgpu-hal` via the Bevy
|
||||||
|
dev-dependency.
|
||||||
|
- Allocator state: `crates/renderer_device/src/device.rs:151`
|
||||||
|
`pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>`, initialized at
|
||||||
|
`:441-456` (`AllocatorCreateDesc`, `AllocationSizes::new(8MB,64MB)` with 256MB
|
||||||
|
maxima). `GpuAllocation` is the private alias at `:111`; `AllocationStrategy`
|
||||||
|
at `:114-124`; `Allocation::{Owned,Shared,Unmanaged}` at `:126-147`. Free path
|
||||||
|
is `ExternallyManagedObject<Device> for GpuAllocation` at `:1074-1077`, which
|
||||||
|
calls `alloc2.lock().free(...)`.
|
||||||
|
- Errors: `crates/renderer_device/src/lib.rs:81`
|
||||||
|
`GpuAllocatorError(#[from] gpu_allocator::AllocationError)`.
|
||||||
|
- Buffer flow: `crates/renderer_resources/src/buffers.rs:4` imports
|
||||||
|
`AllocationScheme`; `:20,68` `MemoryLocation`; `:108-122` allocates from
|
||||||
|
`alloc2`; `:128-130` binds with raw `vkBindBufferMemory`; `:137` wraps in
|
||||||
|
`Allocation::Owned(DeviceObject<GpuAllocation>)`; `:164-181` maps via
|
||||||
|
`mapped_slice`/`mapped_slice_mut`.
|
||||||
|
- Image flow: `crates/renderer_resources/src/images.rs:16` imports VMA-named
|
||||||
|
`AllocationCreateDesc/Scheme`; `:35,126,289` `MemoryLocation`; `:191-205`
|
||||||
|
allocates and wraps; `:217-223` binds; `:244-260` validates size/memory-type
|
||||||
|
for `new_with_allocation`; `:418-428` exposes allocation.
|
||||||
|
- Dedicated logic: `buffers.rs:116-121` uses
|
||||||
|
`AllocationScheme::DedicatedBuffer(buffer)`; `images.rs:196-199` uses
|
||||||
|
`DedicatedImage(image)`; `renderer_rendergraph/src/graph.rs:1352-1357` chooses
|
||||||
|
dedicated at 64 MiB.
|
||||||
|
- Direct test allocations: `crates/renderer_device/src/testing.rs:312-319` and
|
||||||
|
`:351-357` allocate/free directly through `device.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`; tests
|
||||||
|
`renderer_resources/tests/managed_set_validation.rs:26` and
|
||||||
|
`renderer_rendergraph/tests/graph_validation.rs:31`.
|
||||||
|
- No stats/reporting or defragmentation call sites exist today. `Buffer`/`Image`
|
||||||
|
drop field order: buffer/image handle first, then allocation
|
||||||
|
(`buffers.rs:74-78`, `images.rs:144-148`).
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
1. **Swap allocator inside `renderer_device`, add a private wrapper and a
|
||||||
|
workspace-owned `MemoryLocation` (chosen)**: least invasive, upper crates stay
|
||||||
|
VMA-free.
|
||||||
|
2. Re-export `vk_mem` types as public resource API: least code, but leaks VMA
|
||||||
|
into `renderer_resources`/`renderer` and locks the workspace API to `vk-mem`.
|
||||||
|
3. Use `vk_mem::create_buffer/create_image` directly in `renderer_resources`:
|
||||||
|
fewer Vulkan calls and idiomatic VMA, but larger ownership refactor and
|
||||||
|
couples the resource crate to `vk_mem`.
|
||||||
|
4. 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.0` bundles **VMA 3.3.0** and depends on **ash 0.38** (workspace ash
|
||||||
|
is `0.38.0+1.3.281`), so there is one ash and no duplicate-ash problem.
|
||||||
|
- `vk_mem::Allocator` is `Send + Sync` and internally synchronized unless
|
||||||
|
`EXTERNALLY_SYNCHRONIZED` is set; keep the `Mutex`.
|
||||||
|
- `vk_mem::Allocation` is `Copy, Send, Sync` and has no inherent
|
||||||
|
`size/memory/offset` accessors, so the wrapper must retain `AllocationInfo`.
|
||||||
|
|
||||||
|
## Target design
|
||||||
|
|
||||||
|
- `renderer_device::device::MemoryLocation { Unknown, GpuOnly, CpuToGpu,
|
||||||
|
GpuToCpu }` with `Default = Unknown`, `Copy/Eq/Hash`; use it as the public
|
||||||
|
`mem_location` type.
|
||||||
|
- `pub struct GpuAllocation { raw: vk_mem::Allocation, memory: vk::DeviceMemory,
|
||||||
|
offset: u64, size: u64, memory_type_index: u32, mapped: Option<SendSyncPtr> }`.
|
||||||
|
`SendSyncPtr` is a private `NonNull<u8>` wrapper with unsafe `Send/Sync`,
|
||||||
|
mirroring `gpu_allocator`'s pointer wrapper. Implement `Debug`,
|
||||||
|
`memory()/offset()/size()/memory_type_index()/mapped_slice()/mapped_slice_mut()`.
|
||||||
|
- `pub struct AllocationDesc<'a> { name: &'a str, location: MemoryLocation,
|
||||||
|
strategy: AllocationStrategy }` and
|
||||||
|
`DeviceInner::{allocate_for_buffer, allocate_for_image, bind_buffer_memory,
|
||||||
|
bind_image_memory}`. Note the call shapes: `allocate_memory_for_buffer/image`
|
||||||
|
live on the `vk_mem::Alloc` trait (import `vk_mem::Alloc as _`) and are
|
||||||
|
`unsafe` in `vk-mem 0.5.0`; `bind_buffer_memory`/`bind_image_memory` are
|
||||||
|
inherent (also `unsafe`) `Allocator` methods, not trait methods.
|
||||||
|
- `DeviceInner.alloc2: Mutex<vk_mem::Allocator>` remains the single allocator
|
||||||
|
owner. Keep `Allocation::{Owned(DeviceObject<GpuAllocation>),
|
||||||
|
Shared(Arc<...>), Unmanaged}` unchanged in shape.
|
||||||
|
- `renderer_resources`/`renderer`/`renderer_rendergraph` use only
|
||||||
|
`renderer_device::device::MemoryLocation`, `Allocation`, and the new `Device`
|
||||||
|
helpers; no `vk_mem` imports outside `renderer_device`.
|
||||||
|
|
||||||
|
Suggested interface:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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`: add `vk-mem = "0.5.0"` to `[workspace.dependencies]` (the
|
||||||
|
commented line at `:44` anticipates this); remove the git `gpu-allocator` entry
|
||||||
|
in phase 3.
|
||||||
|
- `renderer_device/Cargo.toml`: add `vk-mem = { workspace = true }`; remove
|
||||||
|
`gpu-allocator` in phase 2 (after the swap compiles).
|
||||||
|
- `renderer_resources/Cargo.toml`, `renderer/Cargo.toml`,
|
||||||
|
`renderer_rendergraph/Cargo.toml`: remove `gpu-allocator` in phase 3.
|
||||||
|
- Ash alignment: `vk-mem 0.5.0` pins ash 0.38; an ash upgrade requires a
|
||||||
|
matching `vk-mem` release 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 `GpuAllocation` is `Send + Sync`.
|
||||||
|
- Add handle guards: wrap raw buffer/image handles in `DeviceObject` right after
|
||||||
|
creation and establish allocation ownership before binding, so allocation and
|
||||||
|
bind failures never leak (see implementation notes).
|
||||||
|
- Add a validation test for `CpuToGpu` mapping that asserts the allocated memory
|
||||||
|
type is `HOST_VISIBLE | HOST_COHERENT` and round-trips a CPU write through
|
||||||
|
`map_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 `alloc2` to `allocator`.
|
||||||
|
|
||||||
|
Verify: full checklist below.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
- Allocator creation: `unsafe { vk_mem::Allocator::new(create_info) }?`; set
|
||||||
|
`preferred_large_heap_block_size` to ~64 MiB (VMA's default is 256 MiB).
|
||||||
|
`vulkan_api_version` must not exceed the instance's requested version or the
|
||||||
|
selected physical device's supported version: record the instance request
|
||||||
|
(add `pub api_version: u32` to `InstanceInner` when building `ApplicationInfo`
|
||||||
|
in `instance.rs:126-135`) and clamp it with the physical device's
|
||||||
|
`limits.api_version` at 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 require
|
||||||
|
`VK_KHR_dedicated_allocation` (+ `VK_KHR_get_memory_requirements2`) at device
|
||||||
|
creation or skip `DEDICATED_MEMORY` there.
|
||||||
|
- Do not enable `EXTERNALLY_SYNCHRONIZED`; keep the `Mutex`.
|
||||||
|
- Allocation flags: `MAPPED` for `CpuToGpu`/`GpuToCpu`, plus `DEDICATED_MEMORY`
|
||||||
|
for `AllocationStrategy::Dedicated`.
|
||||||
|
- **Coherence policy (required)**: VMA's `CpuToGpu`/`GpuToCpu` usages 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 with
|
||||||
|
`HOST_VISIBLE | HOST_COHERENT` from the physical device's memory properties
|
||||||
|
and set `AllocationCreateInfo.memory_type_bits` for 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 explicit
|
||||||
|
`Allocator::flush_allocation`/`invalidate_allocation` around 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 in `DeviceObject` immediately after creation and establish
|
||||||
|
`Allocation` ownership (`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, then
|
||||||
|
`free_memory`). No raw handle may escape a fallible span unowned.
|
||||||
|
- Free with `free_memory` after the buffer/image handle is destroyed; do not
|
||||||
|
also call `destroy_buffer/destroy_image` for the same allocation.
|
||||||
|
|
||||||
|
## Validation checklist
|
||||||
|
|
||||||
|
- `cargo check -p renderer_device`, `-p renderer_resources`, `-p renderer`,
|
||||||
|
`-p renderer_rendergraph`, then `cargo check --workspace`.
|
||||||
|
- `cargo test -p renderer_device` and `cargo test -p renderer_device --features
|
||||||
|
testing` (real Vulkan/validation; skips if unavailable).
|
||||||
|
- `cargo test -p renderer_resources` and `cargo test -p renderer_rendergraph`
|
||||||
|
(both exercise `TestVulkan` through their dev-dependency features).
|
||||||
|
- `cargo tree -d | rg '^ash'` should show one ash. The lockfile has two
|
||||||
|
`gpu-allocator 0.28.0` packages (registry + git), so `cargo tree -i
|
||||||
|
gpu-allocator` is 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 only `wgpu-hal` via the Bevy dev-dependency, and
|
||||||
|
`cargo 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::Allocator` is a raw-pointer owner; it must outlive
|
||||||
|
every `Allocation`. The existing `DeviceInner` field order (`alloc2` before
|
||||||
|
`_drop`) is correct; do not move the allocator after `_drop`. `GpuAllocation`
|
||||||
|
must not be `Copy` and must free exactly once through
|
||||||
|
`DeviceObject`/`ExternallyManagedObject`.
|
||||||
|
- **Thread safety**: `AllocationInfo.mapped_data` is `*mut c_void`, which is not
|
||||||
|
`Send/Sync`; storing it directly breaks `Arc<Buffer>` thread-safety. Use a
|
||||||
|
private `SendSyncPtr` or unsafe `Send/Sync` wrapper and add a compile-time
|
||||||
|
`assert_send_sync::<Buffer>()`/`Image` test.
|
||||||
|
- **Mapping differs**: `gpu-allocator` mapped host-visible blocks
|
||||||
|
opportunistically; VMA maps only with `MAPPED`. `CpuToGpu`/`GpuToCpu` keep
|
||||||
|
working; `GpuOnly` mapping on integrated GPUs may now return `None`.
|
||||||
|
**Coherence**: VMA only guarantees host-visible (not host-coherent) memory
|
||||||
|
for CPU usages; this plan requires coherent types via `memory_type_bits`
|
||||||
|
(see implementation notes) so the existing flush-free `map`/`map_mut` usage
|
||||||
|
stays correct. Non-coherent support would need explicit
|
||||||
|
`flush_allocation`/`invalidate_allocation`; current code does not
|
||||||
|
flush/invalidate either.
|
||||||
|
- **Dedicated allocation**: use `allocate_memory_for_buffer/image` with
|
||||||
|
`DEDICATED_MEMORY`; generic `allocate_memory` would create a separate block
|
||||||
|
without dedicated buffer/image info. `VkMemoryDedicatedAllocateInfo` is 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::AllocationError` variants disappear; VMA
|
||||||
|
returns `vk::Result` (`ERROR_OUT_OF_DEVICE_MEMORY`,
|
||||||
|
`ERROR_FEATURE_NOT_PRESENT`, etc.), already handled by `Error::Result`. No
|
||||||
|
call site pattern-matches the old variant.
|
||||||
|
- **Build environment**: `vk-mem 0.5.0` compiles 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 matching `vk-mem` release/fork. The crate is less actively
|
||||||
|
maintained than `gpu-allocator`.
|
||||||
|
- **Lost features**: no `vk_mem` allocation naming setter
|
||||||
|
(`vmaSetAllocationName`) and no `build_stats_string` in 0.5.0; VMA
|
||||||
|
stats/heap budgets remain available. Defragmentation is intentionally out of
|
||||||
|
scope; if enabled later, cached `memory/offset/mapped_data` in the wrapper
|
||||||
|
must be re-queried.
|
||||||
|
- **Pre-existing hazard**: `Image::new_with_allocation` still creates the raw
|
||||||
|
image before validation (`images.rs:235-260`); the handle-guard requirement
|
||||||
|
(implementation notes) covers this by owning the raw image in `DeviceObject`
|
||||||
|
from creation onward.
|
||||||
Loading…
Reference in a new issue