Skip to main content

gpu_allocator/allocator/free_list_allocator/
mod.rs

1#![deny(unsafe_code, clippy::unwrap_used)]
2#[cfg(feature = "std")]
3use alloc::sync::Arc;
4use alloc::{
5    borrow::ToOwned,
6    string::{String, ToString},
7    vec::Vec,
8};
9#[cfg(feature = "std")]
10use std::backtrace::Backtrace;
11#[cfg(all(feature = "std", not(feature = "hashbrown")))]
12use std::collections::{HashMap, HashSet};
13
14#[cfg(feature = "hashbrown")]
15use hashbrown::{HashMap, HashSet};
16use log::{log, Level};
17
18#[cfg(feature = "visualizer")]
19pub(crate) mod visualizer;
20
21use super::{AllocationReport, AllocationType, SubAllocator, SubAllocatorBase};
22use crate::{AllocationError, Result};
23
24const USE_BEST_FIT: bool = true;
25
26fn align_down(val: u64, alignment: u64) -> u64 {
27    val & !(alignment - 1u64)
28}
29
30fn align_up(val: u64, alignment: u64) -> u64 {
31    align_down(val + alignment - 1u64, alignment)
32}
33
34#[derive(Debug)]
35pub(crate) struct MemoryChunk {
36    pub(crate) chunk_id: core::num::NonZeroU64,
37    pub(crate) size: u64,
38    pub(crate) offset: u64,
39    pub(crate) allocation_type: AllocationType,
40    pub(crate) name: Option<String>,
41    /// Only used if [`crate::AllocatorDebugSettings::store_stack_traces`] is [`true`]
42    #[cfg(feature = "std")]
43    pub(crate) backtrace: Arc<Backtrace>,
44    next: Option<core::num::NonZeroU64>,
45    prev: Option<core::num::NonZeroU64>,
46}
47
48#[derive(Debug)]
49pub(crate) struct FreeListAllocator {
50    size: u64,
51    allocated: u64,
52    pub(crate) chunk_id_counter: u64,
53    pub(crate) chunks: HashMap<core::num::NonZeroU64, MemoryChunk>,
54    free_chunks: HashSet<core::num::NonZeroU64>,
55}
56
57/// Test if two suballocations will overlap the same page.
58fn is_on_same_page(offset_a: u64, size_a: u64, offset_b: u64, page_size: u64) -> bool {
59    let end_a = offset_a + size_a - 1;
60    let end_page_a = align_down(end_a, page_size);
61    let start_b = offset_b;
62    let start_page_b = align_down(start_b, page_size);
63
64    end_page_a == start_page_b
65}
66
67/// Test if two allocation types will be conflicting or not.
68fn has_granularity_conflict(type0: AllocationType, type1: AllocationType) -> bool {
69    if type0 == AllocationType::Free || type1 == AllocationType::Free {
70        return false;
71    }
72
73    type0 != type1
74}
75
76impl FreeListAllocator {
77    pub(crate) fn new(size: u64) -> Self {
78        #[allow(clippy::unwrap_used)]
79        let initial_chunk_id = core::num::NonZeroU64::new(1).unwrap();
80
81        let mut chunks = HashMap::default();
82        chunks.insert(
83            initial_chunk_id,
84            MemoryChunk {
85                chunk_id: initial_chunk_id,
86                size,
87                offset: 0,
88                allocation_type: AllocationType::Free,
89                name: None,
90                #[cfg(feature = "std")]
91                backtrace: Arc::new(Backtrace::disabled()),
92                prev: None,
93                next: None,
94            },
95        );
96
97        let mut free_chunks = HashSet::default();
98        free_chunks.insert(initial_chunk_id);
99
100        Self {
101            size,
102            allocated: 0,
103            // 0 is not allowed as a chunk ID, 1 is used by the initial chunk, next chunk is going to be 2.
104            // The system well take the counter as the ID, and the increment the counter.
105            chunk_id_counter: 2,
106            chunks,
107            free_chunks,
108        }
109    }
110
111    /// Generates a new unique chunk ID
112    fn get_new_chunk_id(&mut self) -> Result<core::num::NonZeroU64> {
113        if self.chunk_id_counter == u64::MAX {
114            // End of chunk id counter reached, no more allocations are possible.
115            return Err(AllocationError::OutOfMemory);
116        }
117
118        let id = self.chunk_id_counter;
119        self.chunk_id_counter += 1;
120        core::num::NonZeroU64::new(id).ok_or_else(|| {
121            AllocationError::Internal("New chunk id was 0, which is not allowed.".into())
122        })
123    }
124    /// Finds the specified `chunk_id` in the list of free chunks and removes if from the list
125    fn remove_id_from_free_list(&mut self, chunk_id: core::num::NonZeroU64) {
126        self.free_chunks.remove(&chunk_id);
127    }
128    /// Merges two adjacent chunks. Right chunk will be merged into the left chunk
129    fn merge_free_chunks(
130        &mut self,
131        chunk_left: core::num::NonZeroU64,
132        chunk_right: core::num::NonZeroU64,
133    ) -> Result<()> {
134        // Gather data from right chunk and remove it
135        let (right_size, right_next) = {
136            let chunk = self.chunks.remove(&chunk_right).ok_or_else(|| {
137                AllocationError::Internal("Chunk ID not present in chunk list.".into())
138            })?;
139            self.remove_id_from_free_list(chunk.chunk_id);
140
141            (chunk.size, chunk.next)
142        };
143
144        // Merge into left chunk
145        {
146            let chunk = self.chunks.get_mut(&chunk_left).ok_or_else(|| {
147                AllocationError::Internal("Chunk ID not present in chunk list.".into())
148            })?;
149            chunk.next = right_next;
150            chunk.size += right_size;
151        }
152
153        // Patch pointers
154        if let Some(right_next) = right_next {
155            let chunk = self.chunks.get_mut(&right_next).ok_or_else(|| {
156                AllocationError::Internal("Chunk ID not present in chunk list.".into())
157            })?;
158            chunk.prev = Some(chunk_left);
159        }
160
161        Ok(())
162    }
163}
164
165impl SubAllocatorBase for FreeListAllocator {}
166impl SubAllocator for FreeListAllocator {
167    fn allocate(
168        &mut self,
169        size: u64,
170        alignment: u64,
171        allocation_type: AllocationType,
172        granularity: u64,
173        name: &str,
174        #[cfg(feature = "std")] backtrace: Arc<Backtrace>,
175    ) -> Result<(u64, core::num::NonZeroU64)> {
176        let free_size = self.size - self.allocated;
177        if size > free_size {
178            return Err(AllocationError::OutOfMemory);
179        }
180
181        let mut best_fit_id: Option<core::num::NonZeroU64> = None;
182        let mut best_offset = 0u64;
183        let mut best_aligned_size = 0u64;
184        let mut best_chunk_size = 0u64;
185
186        for current_chunk_id in self.free_chunks.iter() {
187            let current_chunk = self.chunks.get(current_chunk_id).ok_or_else(|| {
188                AllocationError::Internal(
189                    "Chunk ID in free list is not present in chunk list.".into(),
190                )
191            })?;
192
193            if current_chunk.size < size {
194                continue;
195            }
196
197            let mut offset = align_up(current_chunk.offset, alignment);
198
199            if let Some(prev_idx) = current_chunk.prev {
200                let previous = self.chunks.get(&prev_idx).ok_or_else(|| {
201                    AllocationError::Internal("Invalid previous chunk reference.".into())
202                })?;
203                if is_on_same_page(previous.offset, previous.size, offset, granularity)
204                    && has_granularity_conflict(previous.allocation_type, allocation_type)
205                {
206                    offset = align_up(offset, granularity);
207                }
208            }
209
210            let padding = offset - current_chunk.offset;
211            let aligned_size = padding + size;
212
213            if aligned_size > current_chunk.size {
214                continue;
215            }
216
217            if let Some(next_idx) = current_chunk.next {
218                let next = self.chunks.get(&next_idx).ok_or_else(|| {
219                    AllocationError::Internal("Invalid next chunk reference.".into())
220                })?;
221                if is_on_same_page(offset, size, next.offset, granularity)
222                    && has_granularity_conflict(allocation_type, next.allocation_type)
223                {
224                    continue;
225                }
226            }
227
228            if USE_BEST_FIT {
229                if best_fit_id.is_none() || current_chunk.size < best_chunk_size {
230                    best_fit_id = Some(*current_chunk_id);
231                    best_aligned_size = aligned_size;
232                    best_offset = offset;
233
234                    best_chunk_size = current_chunk.size;
235                };
236            } else {
237                best_fit_id = Some(*current_chunk_id);
238                best_aligned_size = aligned_size;
239                best_offset = offset;
240
241                best_chunk_size = current_chunk.size;
242                break;
243            }
244        }
245
246        let first_fit_id = best_fit_id.ok_or(AllocationError::OutOfMemory)?;
247
248        let chunk_id = if best_chunk_size > best_aligned_size {
249            let new_chunk_id = self.get_new_chunk_id()?;
250
251            let new_chunk = {
252                let free_chunk = self.chunks.get_mut(&first_fit_id).ok_or_else(|| {
253                    AllocationError::Internal("Chunk ID must be in chunk list.".into())
254                })?;
255                let new_chunk = MemoryChunk {
256                    chunk_id: new_chunk_id,
257                    size: best_aligned_size,
258                    offset: free_chunk.offset,
259                    allocation_type,
260                    name: Some(name.to_string()),
261                    #[cfg(feature = "std")]
262                    backtrace,
263                    prev: free_chunk.prev,
264                    next: Some(first_fit_id),
265                };
266
267                free_chunk.prev = Some(new_chunk.chunk_id);
268                free_chunk.offset += best_aligned_size;
269                free_chunk.size -= best_aligned_size;
270                new_chunk
271            };
272
273            if let Some(prev_id) = new_chunk.prev {
274                let prev_chunk = self.chunks.get_mut(&prev_id).ok_or_else(|| {
275                    AllocationError::Internal("Invalid previous chunk reference.".into())
276                })?;
277                prev_chunk.next = Some(new_chunk.chunk_id);
278            }
279
280            self.chunks.insert(new_chunk_id, new_chunk);
281
282            new_chunk_id
283        } else {
284            let chunk = self
285                .chunks
286                .get_mut(&first_fit_id)
287                .ok_or_else(|| AllocationError::Internal("Invalid chunk reference.".into()))?;
288
289            chunk.allocation_type = allocation_type;
290            chunk.name = Some(name.to_string());
291            #[cfg(feature = "std")]
292            {
293                chunk.backtrace = backtrace;
294            }
295
296            self.remove_id_from_free_list(first_fit_id);
297
298            first_fit_id
299        };
300
301        self.allocated += best_aligned_size;
302
303        Ok((best_offset, chunk_id))
304    }
305
306    fn free(&mut self, chunk_id: Option<core::num::NonZeroU64>) -> Result<()> {
307        let chunk_id = chunk_id
308            .ok_or_else(|| AllocationError::Internal("Chunk ID must be a valid value.".into()))?;
309
310        let (next_id, prev_id) = {
311            let chunk = self.chunks.get_mut(&chunk_id).ok_or_else(|| {
312                AllocationError::Internal(
313                    "Attempting to free chunk that is not in chunk list.".into(),
314                )
315            })?;
316            chunk.allocation_type = AllocationType::Free;
317            chunk.name = None;
318            #[cfg(feature = "std")]
319            {
320                chunk.backtrace = Arc::new(Backtrace::disabled());
321            }
322
323            self.allocated -= chunk.size;
324
325            self.free_chunks.insert(chunk.chunk_id);
326
327            (chunk.next, chunk.prev)
328        };
329
330        if let Some(next_id) = next_id {
331            if self.chunks[&next_id].allocation_type == AllocationType::Free {
332                self.merge_free_chunks(chunk_id, next_id)?;
333            }
334        }
335
336        if let Some(prev_id) = prev_id {
337            if self.chunks[&prev_id].allocation_type == AllocationType::Free {
338                self.merge_free_chunks(prev_id, chunk_id)?;
339            }
340        }
341        Ok(())
342    }
343
344    fn rename_allocation(
345        &mut self,
346        chunk_id: Option<core::num::NonZeroU64>,
347        name: &str,
348    ) -> Result<()> {
349        let chunk_id = chunk_id
350            .ok_or_else(|| AllocationError::Internal("Chunk ID must be a valid value.".into()))?;
351
352        let chunk = self.chunks.get_mut(&chunk_id).ok_or_else(|| {
353            AllocationError::Internal(
354                "Attempting to rename chunk that is not in chunk list.".into(),
355            )
356        })?;
357
358        if chunk.allocation_type == AllocationType::Free {
359            return Err(AllocationError::Internal(
360                "Attempting to rename a freed allocation.".into(),
361            ));
362        }
363
364        chunk.name = Some(name.into());
365
366        Ok(())
367    }
368
369    fn report_memory_leaks(
370        &self,
371        log_level: Level,
372        memory_type_index: usize,
373        memory_block_index: usize,
374    ) {
375        for (chunk_id, chunk) in self.chunks.iter() {
376            if chunk.allocation_type == AllocationType::Free {
377                continue;
378            }
379            let empty = "".to_string();
380            let name = chunk.name.as_ref().unwrap_or(&empty);
381            let backtrace_info;
382            #[cfg(feature = "std")]
383            {
384                // TODO: Allocation could be avoided here if https://github.com/rust-lang/rust/pull/139135 is merged and stabilized.
385                backtrace_info = format!(
386                    ",
387        backtrace: {}",
388                    chunk.backtrace
389                )
390            }
391            #[cfg(not(feature = "std"))]
392            {
393                backtrace_info = ""
394            }
395            log!(
396                log_level,
397                r#"leak detected: {{
398    memory type: {}
399    memory block: {}
400    chunk: {{
401        chunk_id: {},
402        size: 0x{:x},
403        offset: 0x{:x},
404        allocation_type: {:?},
405        name: {}{backtrace_info}
406    }}
407}}"#,
408                memory_type_index,
409                memory_block_index,
410                chunk_id,
411                chunk.size,
412                chunk.offset,
413                chunk.allocation_type,
414                name,
415            );
416        }
417    }
418
419    fn report_allocations(&self) -> Vec<AllocationReport> {
420        self.chunks
421            .iter()
422            .filter(|(_key, chunk)| chunk.allocation_type != AllocationType::Free)
423            .map(|(_key, chunk)| AllocationReport {
424                name: chunk
425                    .name
426                    .clone()
427                    .unwrap_or_else(|| "<Unnamed FreeList allocation>".to_owned()),
428                offset: chunk.offset,
429                size: chunk.size,
430                #[cfg(feature = "visualizer")]
431                backtrace: chunk.backtrace.clone(),
432            })
433            .collect::<Vec<_>>()
434    }
435
436    fn allocated(&self) -> u64 {
437        self.allocated
438    }
439
440    fn supports_general_allocations(&self) -> bool {
441        true
442    }
443}