psx_rt/lib.rs
1// SPDX-License-Identifier: GPL-2.0-or-later
2//! PS1 bare-metal runtime.
3//!
4//! Provides the `_start` entry point that the PSX-EXE loader jumps to,
5//! a panic handler, direct i-cache control, a BIOS TTY debug
6//! trampoline, and optional heap initialisation. Homebrew crates
7//! depend on this crate to get a working `main()` environment without
8//! thinking about linker symbols or cache-flushing.
9//!
10//! # Entry sequence
11//!
12//! 1. The loader (our emulator or the real BIOS) copies the EXE
13//! payload to `LOAD_ADDR` and jumps to [`_start`].
14//! 2. [`_start`] zeroes the `.bss` section using the linker-defined
15//! `__bss_start` / `__bss_end` symbols.
16//! 3. With the `alloc` feature, the bump allocator is seeded from
17//! `__heap_start..__heap_end`.
18//! 4. `main()` is called. When it returns, we halt.
19
20#![no_std]
21#![deny(unsafe_op_in_unsafe_fn)]
22#![warn(missing_docs)]
23// Required for MIPS `global_asm!` blocks (BIOS trampolines). Suppress
24// the "feature is declared but not used" warning that shows on the
25// host target where only the panic handler + heap are live.
26#![cfg_attr(target_arch = "mips", feature(asm_experimental_arch))]
27
28// Corrected signed 64-bit divide/mod (compiler-builtins' are broken here).
29#[cfg(target_arch = "mips")]
30mod builtins;
31// Hand-scheduled memcpy/memset/memcmp (compiler-builtins' are generic loops).
32#[cfg(target_arch = "mips")]
33mod mem;
34
35#[cfg(target_arch = "mips")]
36pub mod bios;
37pub mod cache;
38pub mod critical_section;
39pub mod interrupts;
40mod peripherals;
41#[cfg(feature = "present-queue")]
42pub mod present;
43pub mod scratchpad;
44#[cfg(target_arch = "mips")]
45pub mod tty;
46#[allow(
47 dead_code,
48 reason = "used by the MIPS waits and the present queue, which a host build lacks"
49)]
50mod wait;
51
52#[cfg(feature = "alloc")]
53pub mod heap;
54
55pub use peripherals::Peripherals;
56
57/// Registers psx-rt's critical section as the `critical-section` crate's
58/// implementation, so `critical_section::with` anywhere in the program masks
59/// interrupts. Off by default: a program links at most one implementation.
60#[cfg(feature = "critical-section")]
61mod critical_section_impl {
62 struct PsxRt;
63 critical_section::set_impl!(PsxRt);
64
65 // SAFETY: psx-rt's acquire/release pair masks interrupts and restores
66 // the previous state, nesting like brackets, which is the crate's
67 // contract.
68 unsafe impl critical_section::Impl for PsxRt {
69 unsafe fn acquire() -> critical_section::RawRestoreState {
70 // SAFETY: the crate pairs every acquire with one release.
71 let state = unsafe { crate::critical_section::acquire() };
72 crate::critical_section::RestoreState::was_enabled(state)
73 }
74
75 unsafe fn release(restore: critical_section::RawRestoreState) {
76 // SAFETY: `restore` came from the matching acquire.
77 unsafe {
78 crate::critical_section::release(
79 crate::critical_section::RestoreState::from_enabled(restore),
80 )
81 }
82 }
83 }
84}
85
86/// Post-link home for R3000 load-delay hazard trampolines.
87///
88/// LLVM's MIPS delay-slot filler leaves loads in branch delay slots whose
89/// consumer runs one instruction later, inside the load delay, and so reads
90/// the stale register. `hazard-patch` rewrites each such branch to
91/// jump through a short trampoline it writes here after the link (magic,
92/// capacity, then code words), which the staged guest build runs and verifies
93/// instead of disabling the filler's backward search at a cost of tens of
94/// kilobytes of nops. Every guest that links psx-rt therefore carries this
95/// 392-byte array in `.data` (1,032 bytes with `hazard-trampolines-256`); nothing reads it at runtime except the CPU.
96#[no_mangle]
97#[used]
98static mut HAZARD_TRAMPOLINES: [u32; 2 + HAZARD_TRAMPOLINE_WORDS] = {
99 let mut words = [0u32; 2 + HAZARD_TRAMPOLINE_WORDS];
100 words[0] = 0x4841_5a54;
101 words[1] = HAZARD_TRAMPOLINE_WORDS as u32;
102 words
103};
104// Code words available to the patcher. The default suits every shipped
105// guest; a large exe whose link reports "trampoline array full" enables the
106// `hazard-trampolines-256` feature and pays 640 more bytes of `.data`.
107#[cfg(feature = "hazard-trampolines-256")]
108const HAZARD_TRAMPOLINE_WORDS: usize = 256;
109#[cfg(not(feature = "hazard-trampolines-256"))]
110const HAZARD_TRAMPOLINE_WORDS: usize = 96;
111
112// Symbols emitted by `psoxide.ld`.
113extern "C" {
114 static mut __bss_start: u8;
115 static mut __bss_end: u8;
116 #[cfg(feature = "alloc")]
117 static __heap_start: u8;
118 #[cfg(feature = "alloc")]
119 static __heap_end: u8;
120}
121
122#[cfg(target_arch = "mips")]
123extern "Rust" {
124 fn main();
125}
126
127#[cfg(target_arch = "mips")]
128#[used]
129#[link_section = ".region"]
130static PSX_EXE_REGION: [u8; 55] = *b"PSoXide SDK homebrew executable ";
131
132/// Entry point the PSX-EXE loader jumps to.
133///
134/// The arguments are the MIPS ABI's first three registers as the loader left
135/// them. A BIOS boot leaves garbage there, which is why
136/// [`psx_io::disc_base::install`] gates on a magic value rather than trusting
137/// them; a multi-program loader passes
138/// (`HANDOFF_MAGIC`, disc LBA offset, CD-DA track base) so the SDK can find
139/// this program's data on a disc it does not own.
140///
141/// # Safety
142/// Called exactly once at boot by the loader. The caller must
143/// have set up a valid stack pointer before branching here -- the
144/// PSX-EXE header's `initial_sp_base` + `initial_sp_offset` fields
145/// guarantee this.
146#[cfg(target_arch = "mips")]
147#[no_mangle]
148#[link_section = ".text._start"]
149pub unsafe extern "C" fn _start(a0: u32, a1: u32, a2: u32) -> ! {
150 #[cfg(feature = "boot-trace")]
151 tty::println("psx-rt: start");
152
153 // Zero BSS.
154 let bss_start = &raw mut __bss_start;
155 let bss_end = &raw const __bss_end;
156 let bss_len = bss_end as usize - bss_start as usize;
157 if bss_len > 0 {
158 // SAFETY: [__bss_start, __bss_end) is this image's .bss as psoxide.ld lays it out: writable
159 // RAM the program owns. No Rust code has run yet, so nothing reads or references it, and
160 // zero is each .bss static's initial value.
161 unsafe { core::ptr::write_bytes(bss_start, 0, bss_len) };
162 }
163 #[cfg(feature = "boot-trace")]
164 tty::println("psx-rt: bss ok");
165
166 // Before any game code can replace the exception vector: `wait_vblank` installs lazily only
167 // over the word found here.
168 interrupts::record_boot_vector();
169
170 // After the zeroing, not before: the bases live in statics.
171 // SAFETY: once, at boot, before anything can observe them.
172 unsafe { psx_io::disc_base::install(a0, a1, a2) };
173
174 #[cfg(feature = "alloc")]
175 {
176 let heap_start = &raw const __heap_start as usize;
177 let heap_end = &raw const __heap_end as usize;
178 // The RAM region ends 0x100 bytes above __heap_end, so a .bss that ends in that window
179 // puts __heap_start past __heap_end: that leaves no heap, not a wrapped 4 GiB one.
180 let heap_size = heap_end.saturating_sub(heap_start);
181 // SAFETY: runs once, before `main` and any allocation. [heap_start, heap_start +
182 // heap_size) is the range psoxide.ld leaves between .bss and the stack reserve (empty if
183 // they meet), used by nothing else.
184 unsafe { heap::init(heap_start, heap_size) };
185 }
186
187 #[cfg(feature = "boot-trace")]
188 tty::println("psx-rt: main");
189 // SAFETY: the game crate defines `#[no_mangle] fn main()` and the link resolves this
190 // declaration to it. The signature is not checked across that boundary; a `main` with a
191 // different signature would be undefined behaviour.
192 unsafe { main() };
193 #[cfg(feature = "boot-trace")]
194 tty::println("psx-rt: main returned");
195 halt();
196}
197
198/// Current MIPS stack pointer (`$sp`). No memory or stack side effects.
199#[cfg(target_arch = "mips")]
200#[inline(always)]
201fn stack_pointer() -> usize {
202 let sp: usize;
203 // SAFETY: a single register move out of `$sp`.
204 unsafe {
205 // The allocator may pick $at; `.set noat` keeps the assembler quiet.
206 core::arch::asm!(".set noat", "move {}, $sp", ".set at", out(reg) sp, options(nomem, nostack, preserves_flags));
207 }
208 sp
209}
210
211/// Trap (loudly, via TTY) if the live stack pointer has descended into
212/// the static data region.
213///
214/// The linker lays out `.text/.data/.bss` from the load address upward
215/// and runs the stack downward from the top of RAM. The two are only
216/// safe while `$sp` stays above the top of `.bss`. A large
217/// stack-resident object (e.g. a multi-hundred-KB struct held in a
218/// `main` local) or very deep call frames push `$sp` *below* `__bss_end`;
219/// from there a stack write and a write to some `static` alias the same
220/// RAM and silently corrupt each other -- the corruption surfaces far
221/// from the cause and is brutal to debug. Call this once the deepest
222/// expected stack frame is live (e.g. right after constructing the main
223/// scene object) to turn that into an immediate, located halt instead.
224///
225/// Note: this checks against the *actual* top of static data, not the
226/// linker's `__heap_end` stack floor, which currently reserves only a
227/// nominal slice and would false-positive on any real stack.
228#[cfg(target_arch = "mips")]
229pub fn assert_stack_headroom() {
230 /// Bytes of slack required between `$sp` and the top of `.bss`.
231 const GUARD: usize = 0x800;
232 let data_top = &raw const __bss_end as usize;
233 let sp = stack_pointer();
234 if sp <= data_top.saturating_add(GUARD) {
235 tty::print("\nSTACK/DATA COLLISION: $sp=0x");
236 tty::print_hex_u32(sp as u32);
237 tty::print(" is inside static data (top=0x");
238 tty::print_hex_u32(data_top as u32);
239 tty::print(").\n A large stack object or deep call frames overran the stack and\n now alias `static` memory -- this silently corrupts both. Move big\n objects into .bss, shrink static buffers, or enlarge the stack.\n");
240 halt();
241 }
242}
243
244/// Infinite loop with no useful side effects. Used after `main()`
245/// returns or from panic / reset paths.
246#[inline(never)]
247pub fn halt() -> ! {
248 loop {
249 core::hint::spin_loop();
250 }
251}
252
253/// Panic handler. Tries to write the message to TTY so PCSX-Redux
254/// (and our emulator's future console hook) shows it, then halts.
255///
256/// A panic raised on a [`scratchpad::ScratchpadStack`] first moves back to
257/// the RAM stack, so reporting (BIOS `putchar` included) never runs on the
258/// scratchpad. `stack-guard` counts this function's own frame and
259/// nothing below it, which is only sound while the report stays out of line.
260///
261/// Only registered when targeting PS1 hardware. Host builds of the
262/// SDK use `std`'s panic handler -- this avoids the lang-item conflict
263/// that would otherwise fire when something on host transitively
264/// depends on both `psx-rt` and `std`.
265#[cfg(target_arch = "mips")]
266#[panic_handler]
267fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
268 let info = (info as *const core::panic::PanicInfo<'_>).cast::<u8>();
269 scratchpad::leave_for_panic(info, report_panic);
270 report_panic(info)
271}
272
273#[cfg(target_arch = "mips")]
274#[inline(never)]
275extern "C" fn report_panic(info: *const u8) -> ! {
276 // SAFETY: `panic` passes its `&PanicInfo`, alive for the whole panic.
277 let info = unsafe { &*info.cast::<core::panic::PanicInfo<'_>>() };
278 tty::print("PANIC: ");
279 if let Some(msg) = info.message().as_str() {
280 tty::print(msg);
281 }
282 tty::print("\n");
283 if let Some(loc) = info.location() {
284 tty::print(" at ");
285 tty::print(loc.file());
286 tty::print(":");
287 // Print the line number (no fmt machinery in no_std): up to 8 digits.
288 let mut line = loc.line();
289 let mut buf = [0u8; 8];
290 let mut i = buf.len();
291 if line == 0 {
292 i -= 1;
293 buf[i] = b'0';
294 }
295 while line > 0 && i > 0 {
296 i -= 1;
297 buf[i] = b'0' + (line % 10) as u8;
298 line /= 10;
299 }
300 if let Ok(s) = core::str::from_utf8(&buf[i..]) {
301 tty::print(s);
302 }
303 tty::print("\n");
304 }
305 halt()
306}