Skip to main content

lgpu_hal/vulkan/
allocator.rs

1//! Simple Vulkan memory suballocator.
2//!
3//! Suballocates small buffer and texture memory allocations from large contiguous
4//! blocks (32 MiB by default) to stay well within driver allocation limits.
5
6use crate::HalError;
7use ash::vk;
8
9pub const DEFAULT_BLOCK_SIZE: u64 = 32 * 1024 * 1024; // 32 MiB
10pub const SUBALLOCATION_THRESHOLD: u64 = 16 * 1024 * 1024; // 16 MiB
11
12/// Distinguishes linear resources (buffers) from non-linear resources (optimal textures)
13/// to respect Vulkan bufferImageGranularity rules and prevent memory aliasing.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AllocationType {
16    Linear,
17    Optimal,
18}
19
20#[derive(Debug)]
21pub struct AllocationResult {
22    pub memory: vk::DeviceMemory,
23    pub offset: u64,
24    pub block_id: Option<usize>,
25    pub mapped_ptr: Option<*mut u8>,
26    pub is_coherent: bool,
27}
28
29pub struct MemoryBlock {
30    pub id: usize,
31    pub memory: vk::DeviceMemory,
32    pub size: u64,
33    pub memory_type_index: u32,
34    pub is_host_visible: bool,
35    pub allocation_type: AllocationType,
36    pub mapped_base: Option<*mut u8>,
37    pub is_coherent: bool,
38    pub free_chunks: Vec<(u64, u64)>, // (offset, size)
39}
40
41unsafe impl Send for MemoryBlock {}
42unsafe impl Sync for MemoryBlock {}
43
44impl MemoryBlock {
45    pub fn allocate(&mut self, size: u64, alignment: u64) -> Option<u64> {
46        let align = alignment.max(1);
47        for i in 0..self.free_chunks.len() {
48            let (chunk_offset, chunk_size) = self.free_chunks[i];
49            let aligned_offset = if align.is_power_of_two() {
50                (chunk_offset + align - 1) & !(align - 1)
51            } else {
52                ((chunk_offset + align - 1) / align) * align
53            };
54            let padding = aligned_offset - chunk_offset;
55            if chunk_size >= padding + size {
56                // Remove chunk and insert pre/post gaps
57                self.free_chunks.remove(i);
58                if padding > 0 {
59                    self.free_chunks.push((chunk_offset, padding));
60                }
61                let remaining = chunk_size - (padding + size);
62                if remaining > 0 {
63                    self.free_chunks.push((aligned_offset + size, remaining));
64                }
65                self.free_chunks.sort_unstable_by_key(|&(off, _)| off);
66                return Some(aligned_offset);
67            }
68        }
69        None
70    }
71
72    pub fn free(&mut self, offset: u64, size: u64) {
73        self.free_chunks.push((offset, size));
74        self.free_chunks.sort_unstable_by_key(|&(off, _)| off);
75
76        // Merge contiguous free chunks
77        let mut merged: Vec<(u64, u64)> = Vec::with_capacity(self.free_chunks.len());
78        for chunk in &self.free_chunks {
79            if let Some(last) = merged.last_mut() {
80                if last.0 + last.1 == chunk.0 {
81                    last.1 += chunk.1;
82                    continue;
83                }
84            }
85            merged.push(*chunk);
86        }
87        self.free_chunks = merged;
88    }
89
90    pub fn is_empty(&self) -> bool {
91        self.free_chunks.len() == 1 && self.free_chunks[0] == (0, self.size)
92    }
93}
94
95pub struct VulkanSuballocator {
96    blocks: Vec<MemoryBlock>,
97    next_block_id: usize,
98}
99
100impl VulkanSuballocator {
101    pub fn new() -> Self {
102        Self {
103            blocks: Vec::new(),
104            next_block_id: 0,
105        }
106    }
107
108    pub fn allocate(
109        &mut self,
110        device: &ash::Device,
111        size: u64,
112        alignment: u64,
113        memory_type_index: u32,
114        is_host_visible: bool,
115        is_coherent: bool,
116        allocation_type: AllocationType,
117    ) -> Result<AllocationResult, HalError> {
118        // Large allocations exceed threshold: use dedicated allocation
119        if size >= SUBALLOCATION_THRESHOLD {
120            let alloc_info = vk::MemoryAllocateInfo::default()
121                .allocation_size(size)
122                .memory_type_index(memory_type_index);
123
124            let memory = unsafe {
125                device.allocate_memory(&alloc_info, None).map_err(|e| {
126                    HalError::OutOfMemory(format!("vkAllocateMemory dedicated failed: {e}"))
127                })?
128            };
129
130            let mapped_ptr = if is_host_visible {
131                let ptr = unsafe {
132                    device
133                        .map_memory(memory, 0, size, vk::MemoryMapFlags::empty())
134                        .map_err(|e| HalError::MapError(e.to_string()))?
135                };
136                Some(ptr as *mut u8)
137            } else {
138                None
139            };
140
141            return Ok(AllocationResult {
142                memory,
143                offset: 0,
144                block_id: None,
145                mapped_ptr,
146                is_coherent,
147            });
148        }
149
150        // Search existing blocks for matching memory type, host-visibility, allocation type, and sufficient space
151        for block in &mut self.blocks {
152            if block.memory_type_index == memory_type_index
153                && block.is_host_visible == is_host_visible
154                && block.allocation_type == allocation_type
155            {
156                if let Some(offset) = block.allocate(size, alignment) {
157                    let mapped_ptr = block
158                        .mapped_base
159                        .map(|base| unsafe { base.add(offset as usize) });
160                    return Ok(AllocationResult {
161                        memory: block.memory,
162                        offset,
163                        block_id: Some(block.id),
164                        mapped_ptr,
165                        is_coherent: block.is_coherent,
166                    });
167                }
168            }
169        }
170
171        // Allocate a new block of DEFAULT_BLOCK_SIZE
172        let block_size = DEFAULT_BLOCK_SIZE.max(size);
173        let alloc_info = vk::MemoryAllocateInfo::default()
174            .allocation_size(block_size)
175            .memory_type_index(memory_type_index);
176
177        let memory = unsafe {
178            device
179                .allocate_memory(&alloc_info, None)
180                .map_err(|e| HalError::OutOfMemory(format!("vkAllocateMemory block failed: {e}")))?
181        };
182
183        let mapped_base = if is_host_visible {
184            let ptr = unsafe {
185                device
186                    .map_memory(memory, 0, block_size, vk::MemoryMapFlags::empty())
187                    .map_err(|e| HalError::MapError(e.to_string()))?
188            };
189            Some(ptr as *mut u8)
190        } else {
191            None
192        };
193
194        let block_id = self.next_block_id;
195        self.next_block_id += 1;
196
197        let mut block = MemoryBlock {
198            id: block_id,
199            memory,
200            size: block_size,
201            memory_type_index,
202            is_host_visible,
203            allocation_type,
204            mapped_base,
205            is_coherent,
206            free_chunks: vec![(0, block_size)],
207        };
208
209        let offset = block
210            .allocate(size, alignment)
211            .expect("Fresh block must have sufficient space for allocation");
212
213        let mapped_ptr = block
214            .mapped_base
215            .map(|base| unsafe { base.add(offset as usize) });
216
217        self.blocks.push(block);
218
219        Ok(AllocationResult {
220            memory,
221            offset,
222            block_id: Some(block_id),
223            mapped_ptr,
224            is_coherent,
225        })
226    }
227
228    pub fn free(&mut self, block_id: usize, offset: u64, size: u64, device: &ash::Device) {
229        let mut block_to_reclaim = None;
230
231        if let Some(pos) = self.blocks.iter().position(|b| b.id == block_id) {
232            self.blocks[pos].free(offset, size);
233
234            if self.blocks[pos].is_empty() {
235                let mem_idx = self.blocks[pos].memory_type_index;
236                let is_host_vis = self.blocks[pos].is_host_visible;
237                let alloc_type = self.blocks[pos].allocation_type;
238
239                let empty_count = self
240                    .blocks
241                    .iter()
242                    .filter(|b| {
243                        b.memory_type_index == mem_idx
244                            && b.is_host_visible == is_host_vis
245                            && b.allocation_type == alloc_type
246                            && b.is_empty()
247                    })
248                    .count();
249
250                // Keep at most 1 empty block per configuration as a warm pool to prevent
251                // allocation thrashing. Any redundant empty blocks are immediately reclaimed.
252                if empty_count > 1 {
253                    block_to_reclaim = Some(pos);
254                }
255            }
256        }
257
258        if let Some(pos) = block_to_reclaim {
259            let block = self.blocks.remove(pos);
260            unsafe {
261                if block.mapped_base.is_some() {
262                    device.unmap_memory(block.memory);
263                }
264                device.free_memory(block.memory, None);
265            }
266        }
267    }
268
269    /// Reclaims ALL completely empty blocks, releasing their GPU and host memory back to Vulkan.
270    /// Returns the number of reclaimed blocks.
271    pub fn trim(&mut self, device: &ash::Device) -> usize {
272        let mut freed_count = 0;
273        let mut i = 0;
274        while i < self.blocks.len() {
275            if self.blocks[i].is_empty() {
276                let block = self.blocks.remove(i);
277                unsafe {
278                    if block.mapped_base.is_some() {
279                        device.unmap_memory(block.memory);
280                    }
281                    device.free_memory(block.memory, None);
282                }
283                freed_count += 1;
284            } else {
285                i += 1;
286            }
287        }
288        freed_count
289    }
290
291    /// Returns the total number of blocks currently held by the suballocator.
292    pub fn block_count(&self) -> usize {
293        self.blocks.len()
294    }
295
296    /// Returns the number of completely empty blocks currently held by the suballocator.
297    pub fn empty_block_count(&self) -> usize {
298        self.blocks.iter().filter(|b| b.is_empty()).count()
299    }
300
301    /// Returns the total capacity in bytes across all managed blocks.
302    pub fn total_capacity_bytes(&self) -> u64 {
303        self.blocks.iter().map(|b| b.size).sum()
304    }
305
306    /// Returns the total available free bytes across all managed blocks.
307    pub fn total_free_bytes(&self) -> u64 {
308        self.blocks
309            .iter()
310            .map(|b| b.free_chunks.iter().map(|&(_, sz)| sz).sum::<u64>())
311            .sum()
312    }
313
314    pub fn destroy(&mut self, device: &ash::Device) {
315        unsafe {
316            for block in self.blocks.drain(..) {
317                if block.mapped_base.is_some() {
318                    device.unmap_memory(block.memory);
319                }
320                device.free_memory(block.memory, None);
321            }
322        }
323    }
324}