Skip to main content

lgpu_hal/vulkan/
allocator.rs

1//! High-performance Vulkan memory suballocator.
2//!
3//! Suballocates small buffer and texture memory allocations from large contiguous
4//! blocks (32 MiB by default), avoiding driver allocation limits (e.g., 4096 allocations)
5//! and eliminating redundant vkAllocateMemory, vkMapMemory, and vkUnmapMemory calls.
6
7use crate::HalError;
8use ash::vk;
9
10pub const DEFAULT_BLOCK_SIZE: u64 = 32 * 1024 * 1024; // 32 MiB
11pub const SUBALLOCATION_THRESHOLD: u64 = 16 * 1024 * 1024; // 16 MiB
12
13/// Distinguishes linear resources (buffers) from non-linear resources (optimal textures)
14/// to respect Vulkan bufferImageGranularity rules and prevent memory aliasing.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum AllocationType {
17    Linear,
18    Optimal,
19}
20
21#[derive(Debug)]
22pub struct AllocationResult {
23    pub memory: vk::DeviceMemory,
24    pub offset: u64,
25    pub block_id: Option<usize>,
26    pub mapped_ptr: Option<*mut u8>,
27    pub is_coherent: bool,
28}
29
30pub struct MemoryBlock {
31    pub id: usize,
32    pub memory: vk::DeviceMemory,
33    pub size: u64,
34    pub memory_type_index: u32,
35    pub is_host_visible: bool,
36    pub allocation_type: AllocationType,
37    pub mapped_base: Option<*mut u8>,
38    pub is_coherent: bool,
39    pub free_chunks: Vec<(u64, u64)>, // (offset, size)
40}
41
42unsafe impl Send for MemoryBlock {}
43unsafe impl Sync for MemoryBlock {}
44
45impl MemoryBlock {
46    pub fn allocate(&mut self, size: u64, alignment: u64) -> Option<u64> {
47        let align = alignment.max(1);
48        for i in 0..self.free_chunks.len() {
49            let (chunk_offset, chunk_size) = self.free_chunks[i];
50            let aligned_offset = if align.is_power_of_two() {
51                (chunk_offset + align - 1) & !(align - 1)
52            } else {
53                ((chunk_offset + align - 1) / align) * align
54            };
55            let padding = aligned_offset - chunk_offset;
56            if chunk_size >= padding + size {
57                // Remove chunk and insert pre/post gaps
58                self.free_chunks.remove(i);
59                if padding > 0 {
60                    self.free_chunks.push((chunk_offset, padding));
61                }
62                let remaining = chunk_size - (padding + size);
63                if remaining > 0 {
64                    self.free_chunks.push((aligned_offset + size, remaining));
65                }
66                self.free_chunks.sort_unstable_by_key(|&(off, _)| off);
67                return Some(aligned_offset);
68            }
69        }
70        None
71    }
72
73    pub fn free(&mut self, offset: u64, size: u64) {
74        self.free_chunks.push((offset, size));
75        self.free_chunks.sort_unstable_by_key(|&(off, _)| off);
76
77        // Merge contiguous free chunks
78        let mut merged: Vec<(u64, u64)> = Vec::with_capacity(self.free_chunks.len());
79        for chunk in &self.free_chunks {
80            if let Some(last) = merged.last_mut() {
81                if last.0 + last.1 == chunk.0 {
82                    last.1 += chunk.1;
83                    continue;
84                }
85            }
86            merged.push(*chunk);
87        }
88        self.free_chunks = merged;
89    }
90}
91
92pub struct VulkanSuballocator {
93    blocks: Vec<MemoryBlock>,
94    next_block_id: usize,
95}
96
97impl VulkanSuballocator {
98    pub fn new() -> Self {
99        Self {
100            blocks: Vec::new(),
101            next_block_id: 0,
102        }
103    }
104
105    pub fn allocate(
106        &mut self,
107        device: &ash::Device,
108        size: u64,
109        alignment: u64,
110        memory_type_index: u32,
111        is_host_visible: bool,
112        is_coherent: bool,
113        allocation_type: AllocationType,
114    ) -> Result<AllocationResult, HalError> {
115        // Large allocations exceed threshold: use dedicated allocation
116        if size >= SUBALLOCATION_THRESHOLD {
117            let alloc_info = vk::MemoryAllocateInfo::default()
118                .allocation_size(size)
119                .memory_type_index(memory_type_index);
120
121            let memory = unsafe {
122                device.allocate_memory(&alloc_info, None).map_err(|e| {
123                    HalError::OutOfMemory(format!("vkAllocateMemory dedicated failed: {e}"))
124                })?
125            };
126
127            let mapped_ptr = if is_host_visible {
128                let ptr = unsafe {
129                    device
130                        .map_memory(memory, 0, size, vk::MemoryMapFlags::empty())
131                        .map_err(|e| HalError::MapError(e.to_string()))?
132                };
133                Some(ptr as *mut u8)
134            } else {
135                None
136            };
137
138            return Ok(AllocationResult {
139                memory,
140                offset: 0,
141                block_id: None,
142                mapped_ptr,
143                is_coherent,
144            });
145        }
146
147        // Search existing blocks for matching memory type, host-visibility, allocation type, and sufficient space
148        for block in &mut self.blocks {
149            if block.memory_type_index == memory_type_index
150                && block.is_host_visible == is_host_visible
151                && block.allocation_type == allocation_type
152            {
153                if let Some(offset) = block.allocate(size, alignment) {
154                    let mapped_ptr = block
155                        .mapped_base
156                        .map(|base| unsafe { base.add(offset as usize) });
157                    return Ok(AllocationResult {
158                        memory: block.memory,
159                        offset,
160                        block_id: Some(block.id),
161                        mapped_ptr,
162                        is_coherent: block.is_coherent,
163                    });
164                }
165            }
166        }
167
168        // Allocate a new block of DEFAULT_BLOCK_SIZE
169        let block_size = DEFAULT_BLOCK_SIZE.max(size);
170        let alloc_info = vk::MemoryAllocateInfo::default()
171            .allocation_size(block_size)
172            .memory_type_index(memory_type_index);
173
174        let memory = unsafe {
175            device
176                .allocate_memory(&alloc_info, None)
177                .map_err(|e| HalError::OutOfMemory(format!("vkAllocateMemory block failed: {e}")))?
178        };
179
180        let mapped_base = if is_host_visible {
181            let ptr = unsafe {
182                device
183                    .map_memory(memory, 0, block_size, vk::MemoryMapFlags::empty())
184                    .map_err(|e| HalError::MapError(e.to_string()))?
185            };
186            Some(ptr as *mut u8)
187        } else {
188            None
189        };
190
191        let block_id = self.next_block_id;
192        self.next_block_id += 1;
193
194        let mut block = MemoryBlock {
195            id: block_id,
196            memory,
197            size: block_size,
198            memory_type_index,
199            is_host_visible,
200            allocation_type,
201            mapped_base,
202            is_coherent,
203            free_chunks: vec![(0, block_size)],
204        };
205
206        let offset = block
207            .allocate(size, alignment)
208            .expect("Fresh block must have sufficient space for allocation");
209
210        let mapped_ptr = block
211            .mapped_base
212            .map(|base| unsafe { base.add(offset as usize) });
213
214        self.blocks.push(block);
215
216        Ok(AllocationResult {
217            memory,
218            offset,
219            block_id: Some(block_id),
220            mapped_ptr,
221            is_coherent,
222        })
223    }
224
225    pub fn free(&mut self, block_id: usize, offset: u64, size: u64) {
226        if let Some(block) = self.blocks.iter_mut().find(|b| b.id == block_id) {
227            block.free(offset, size);
228        }
229    }
230
231    pub fn destroy(&mut self, device: &ash::Device) {
232        unsafe {
233            for block in self.blocks.drain(..) {
234                if block.mapped_base.is_some() {
235                    device.unmap_memory(block.memory);
236                }
237                device.free_memory(block.memory, None);
238            }
239        }
240    }
241}