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

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}