Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_rt/
heap.rs

1//! Bump allocator gated behind the `alloc` feature.
2//!
3//! A tiny `GlobalAlloc` that never frees -- fine for PS1 homebrew that
4//! uses a permanent arena for assets and scratch buffers. Replace
5//! with a real allocator (`linked_list_allocator`, `talc`, …) when the
6//! engine needs deallocation.
7
8use core::alloc::{GlobalAlloc, Layout};
9use core::cell::UnsafeCell;
10
11struct BumpAllocator {
12    state: UnsafeCell<BumpState>,
13}
14
15struct BumpState {
16    next: usize,
17    end: usize,
18}
19
20// SAFETY: `state` is reached only through `alloc` and `init`. The PS1 has one
21// CPU and no threads, and `alloc` does not mask interrupts, so the real
22// invariant is that no interrupt handler allocates: psx-rt's exception handler
23// is pure assembly and never does. A game-installed handler that allocated
24// could interleave with an in-progress `alloc` and break this.
25unsafe impl Sync for BumpAllocator {}
26
27// SAFETY: `alloc` returns null or a block aligned to `layout.align()` inside
28// the range handed to `init`; `next` only grows, so blocks never overlap, and
29// `dealloc` never reuses memory.
30unsafe impl GlobalAlloc for BumpAllocator {
31    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
32        // SAFETY: single-threaded access per the `Sync` impl, so this is the
33        // only live reference to the state.
34        unsafe {
35            let state = &mut *self.state.get();
36            let align = layout.align();
37            let size = layout.size();
38            // Checked: on the 32-bit guest a layout near `isize::MAX` (legal
39            // for a safe caller) would otherwise wrap past the `end` test and
40            // hand out a block running off the heap.
41            let Some(end) = state
42                .next
43                .checked_add(align - 1)
44                .map(|bumped| bumped & !(align - 1))
45                .and_then(|aligned| aligned.checked_add(size))
46            else {
47                return core::ptr::null_mut();
48            };
49            let aligned = end - size;
50            if end > state.end {
51                return core::ptr::null_mut();
52            }
53            state.next = end;
54            aligned as *mut u8
55        }
56    }
57
58    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
59        // Bump allocator -- nothing to release until a full reset.
60    }
61}
62
63/// Only registered as *the* allocator when targeting PS1 hardware, for the
64/// same reason the panic handler is (see [`crate`]): only [`crate::_start`]
65/// seeds it, so on the host it would be installed still spanning
66/// `0..0` and fail the first allocation any linking test harness makes.
67/// Host builds use `std`'s allocator. The cfg is true for every guest build,
68/// so the PS1 artifact is unaffected.
69#[cfg_attr(target_arch = "mips", global_allocator)]
70static ALLOCATOR: BumpAllocator = BumpAllocator {
71    state: UnsafeCell::new(BumpState { next: 0, end: 0 }),
72};
73
74/// Seed the allocator from `start`, spanning `size` bytes.
75///
76/// # Safety
77/// Called exactly once from [`crate::_start`] with a heap range that
78/// doesn't overlap anything in use.
79pub unsafe fn init(start: usize, size: usize) {
80    // SAFETY: the caller upholds this fn's `# Safety` (once, from `_start`,
81    // before any allocation), so no other reference to the state exists.
82    unsafe {
83        let state = &mut *ALLOCATOR.state.get();
84        state.next = start;
85        state.end = start + size;
86    }
87}