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
91pub struct VulkanSuballocator {
92    blocks: Vec<MemoryBlock>,
93    next_block_id: usize,
94}
95
96impl VulkanSuballocator {
97    pub fn new() -> Self {
98        Self {
99            blocks: Vec::new(),
100            next_block_id: 0,
101        }
102    }
103
104    pub fn allocate(
105        &mut self,
106        device: &ash::Device,
107        size: u64,
108        alignment: u64,
109        memory_type_index: u32,
110        is_host_visible: bool,
111        is_coherent: bool,
112        allocation_type: AllocationType,
113    ) -> Result<AllocationResult, HalError> {
114        // Large allocations exceed threshold: use dedicated allocation
115        if size >= SUBALLOCATION_THRESHOLD {
116            let alloc_info = vk::MemoryAllocateInfo::default()
117                .allocation_size(size)
118                .memory_type_index(memory_type_index);
119
120            let memory = unsafe {
121                device.allocate_memory(&alloc_info, None).map_err(|e| {
122                    HalError::OutOfMemory(format!("vkAllocateMemory dedicated failed: {e}"))
123                })?
124            };
125
126            let mapped_ptr = if is_host_visible {
127                let ptr = unsafe {
128                    device
129                        .map_memory(memory, 0, size, vk::MemoryMapFlags::empty())
130                        .map_err(|e| HalError::MapError(e.to_string()))?
131                };
132                Some(ptr as *mut u8)
133            } else {
134                None
135            };
136
137            return Ok(AllocationResult {
138                memory,
139                offset: 0,
140                block_id: None,
141                mapped_ptr,
142                is_coherent,
143            });
144        }
145
146        // Search existing blocks for matching memory type, host-visibility, allocation type, and sufficient space
147        for block in &mut self.blocks {
148            if block.memory_type_index == memory_type_index
149                && block.is_host_visible == is_host_visible
150                && block.allocation_type == allocation_type
151            {
152                if let Some(offset) = block.allocate(size, alignment) {
153                    let mapped_ptr = block
154                        .mapped_base
155                        .map(|base| unsafe { base.add(offset as usize) });
156                    return Ok(AllocationResult {
157                        memory: block.memory,
158                        offset,
159                        block_id: Some(block.id),
160                        mapped_ptr,
161                        is_coherent: block.is_coherent,
162                    });
163                }
164            }
165        }
166
167        // Allocate a new block of DEFAULT_BLOCK_SIZE
168        let block_size = DEFAULT_BLOCK_SIZE.max(size);
169        let alloc_info = vk::MemoryAllocateInfo::default()
170            .allocation_size(block_size)
171            .memory_type_index(memory_type_index);
172
173        let memory = unsafe {
174            device
175                .allocate_memory(&alloc_info, None)
176                .map_err(|e| HalError::OutOfMemory(format!("vkAllocateMemory block failed: {e}")))?
177        };
178
179        let mapped_base = if is_host_visible {
180            let ptr = unsafe {
181                device
182                    .map_memory(memory, 0, block_size, vk::MemoryMapFlags::empty())
183                    .map_err(|e| HalError::MapError(e.to_string()))?
184            };
185            Some(ptr as *mut u8)
186        } else {
187            None
188        };
189
190        let block_id = self.next_block_id;
191        self.next_block_id += 1;
192
193        let mut block = MemoryBlock {
194            id: block_id,
195            memory,
196            size: block_size,
197            memory_type_index,
198            is_host_visible,
199            allocation_type,
200            mapped_base,
201            is_coherent,
202            free_chunks: vec![(0, block_size)],
203        };
204
205        let offset = block
206            .allocate(size, alignment)
207            .expect("Fresh block must have sufficient space for allocation");
208
209        let mapped_ptr = block
210            .mapped_base
211            .map(|base| unsafe { base.add(offset as usize) });
212
213        self.blocks.push(block);
214
215        Ok(AllocationResult {
216            memory,
217            offset,
218            block_id: Some(block_id),
219            mapped_ptr,
220            is_coherent,
221        })
222    }
223
224    pub fn free(&mut self, block_id: usize, offset: u64, size: u64) {
225        if let Some(block) = self.blocks.iter_mut().find(|b| b.id == block_id) {
226            block.free(offset, size);
227        }
228    }
229
230    pub fn destroy(&mut self, device: &ash::Device) {
231        unsafe {
232            for block in self.blocks.drain(..) {
233                if block.mapped_base.is_some() {
234                    device.unmap_memory(block.memory);
235                }
236                device.free_memory(block.memory, None);
237            }
238        }
239    }
240}