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}