trapezoid_core/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3mod cdrom;
4mod controller_mem_card;
5mod coprocessor;
6pub mod cpu;
7pub mod gpu;
8mod mdec;
9mod memory;
10mod spu;
11mod timers;
12
13#[cfg(test)]
14mod tests;
15
16use std::{
17    path::{Path, PathBuf},
18    sync::Arc,
19};
20
21use cpu::RegisterType;
22use memory::{Bios, BusLine, CpuBus, Result};
23
24pub use controller_mem_card::DigitalControllerKey;
25
26use crate::gpu::{Device, GpuFuture, Image, Queue};
27
28const MAX_CPU_CYCLES_TO_CLOCK: u32 = 2000;
29
30#[derive(Debug)]
31pub enum PsxError {
32    CouldNotLoadBios,
33    CouldNotLoadDisk(String),
34    DiskTypeNotSupported,
35}
36
37impl std::error::Error for PsxError {}
38impl std::fmt::Display for PsxError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            PsxError::CouldNotLoadBios => write!(f, "Could not load BIOS"),
42            PsxError::CouldNotLoadDisk(s) => write!(f, "Could not load disk: {}", s),
43            PsxError::DiskTypeNotSupported => write!(f, "Disk type not supported"),
44        }
45    }
46}
47
48#[derive(Debug, Clone, Copy)]
49pub struct PsxConfig {
50    pub stdout_debug: bool,
51    pub fast_boot: bool,
52}
53
54pub struct Psx {
55    bus: CpuBus,
56    exe_file: Option<PathBuf>,
57    // used to control when to execute fastboot
58    disk_available: bool,
59    config: PsxConfig,
60    cpu: cpu::Cpu,
61    /// Stores the excess CPU cycles for later execution.
62    ///
63    /// Sometimes, when running the DMA (mostly CD-ROM) it can generate
64    /// a lot of CPU cycles, clocking the components with this many CPU cycles
65    /// will crash the emulator, so we split clocking across multiple `clock` calls.
66    excess_cpu_cycles: u32,
67    cpu_frame_cycles: u32,
68}
69
70impl Psx {
71    // TODO: produce a valid `Error` struct
72    pub fn new<BiosPath: AsRef<Path>, DiskPath: AsRef<Path>>(
73        bios_file_path: BiosPath,
74        disk_file: Option<DiskPath>,
75        config: PsxConfig,
76        device: Arc<Device>,
77        queue: Arc<Queue>,
78    ) -> Result<Self, PsxError> {
79        let bios = Bios::from_file(bios_file_path)?;
80
81        // save the exe file if there is any
82        // The PSX itself is only responsible for loading normal cue files
83        let (exe_file, disk_file) = if let Some(disk_file) = disk_file {
84            let path = disk_file.as_ref().to_owned();
85            // if this is an exe file
86            match path
87                .extension()
88                .unwrap()
89                .to_str()
90                .unwrap()
91                .to_ascii_lowercase()
92                .as_str()
93            {
94                "exe" => (Some(path), None),
95                "cue" => (None, Some(path)),
96                _ => {
97                    return Err(PsxError::DiskTypeNotSupported);
98                }
99            }
100        } else {
101            // only fast_boot if there is anything to run
102            (None, None)
103        };
104
105        Ok(Self {
106            cpu: cpu::Cpu::new(),
107            disk_available: disk_file.is_some(),
108            bus: CpuBus::new(bios, disk_file, config, device, queue)?,
109            exe_file,
110            config,
111            excess_cpu_cycles: 0,
112            cpu_frame_cycles: 0,
113        })
114    }
115
116    pub fn reset(&mut self) {
117        self.cpu.reset();
118        self.bus.reset();
119    }
120
121    #[inline(always)]
122    fn common_clock(&mut self) -> (u32, cpu::CpuState) {
123        let mut cpu_state = cpu::CpuState::Normal;
124        let mut added_clock = 0;
125        if self.excess_cpu_cycles == 0 {
126            // this number doesn't mean anything
127            // TODO: research on when to stop the CPU (maybe fixed number? block of code? other?)
128            let cpu_cycles;
129            let shell_reached;
130
131            (cpu_cycles, cpu_state) = self.cpu.clock(&mut self.bus, 56);
132            shell_reached = self.cpu.is_shell_reached();
133
134            // handle fast booting and hijacking the bios to load exe
135            if shell_reached && (self.config.fast_boot || self.exe_file.is_some()) {
136                if let Some(exe_file) = &self.exe_file {
137                    let (pc, gp, sp_fp) = self.bus.load_exe_in_memory(exe_file);
138
139                    let regs = self.cpu.registers_mut();
140                    println!(
141                        "Loaded EXE {} into pc: {:08x}. gp: {:08x}, sp_fp: {:08x}",
142                        exe_file.display(),
143                        pc,
144                        gp,
145                        sp_fp
146                    );
147
148                    assert_ne!(pc, 0, "PC value cannot be zero");
149                    regs.write(RegisterType::Pc, pc);
150
151                    if gp != 0 {
152                        regs.write(RegisterType::Gp, gp);
153                    }
154                    if sp_fp != 0 {
155                        regs.write(RegisterType::Sp, sp_fp);
156                        regs.write(RegisterType::Fp, sp_fp);
157                    }
158                } else if self.disk_available {
159                    // we are either in a cd game or not, either way, skip the shell
160                    let regs = self.cpu.registers_mut();
161                    // return from the function
162                    regs.write(RegisterType::Pc, regs.read(RegisterType::Ra));
163                }
164            }
165
166            if cpu_cycles == 0 {
167                return (0, cpu_state);
168            }
169            // the DMA is running of the CPU
170            self.excess_cpu_cycles = cpu_cycles + self.bus.clock_dma();
171            added_clock = self.excess_cpu_cycles;
172        }
173
174        let cpu_cycles_to_run = self.excess_cpu_cycles.min(MAX_CPU_CYCLES_TO_CLOCK);
175        self.excess_cpu_cycles -= cpu_cycles_to_run;
176        self.bus.clock_components(cpu_cycles_to_run);
177
178        (added_clock, cpu_state)
179    }
180
181    /// Return `true` if the frame is finished, `false` otherwise.
182    /// Return the CPU state.
183    pub fn clock_based_on_audio(&mut self, max_clocks: u32) -> (bool, cpu::CpuState) {
184        // sync the CPU clocks to the SPU so that the audio would be clearer.
185        const CYCLES_PER_FRAME: u32 = 564480;
186
187        let mut clocks = 0;
188
189        while self.cpu_frame_cycles < CYCLES_PER_FRAME {
190            let (added_clock, cpu_state) = self.common_clock();
191            clocks += added_clock;
192            self.cpu_frame_cycles += added_clock;
193
194            if clocks >= max_clocks || cpu_state != cpu::CpuState::Normal {
195                return (false, cpu_state);
196            }
197        }
198        self.cpu_frame_cycles -= CYCLES_PER_FRAME;
199
200        (true, cpu::CpuState::Normal)
201    }
202
203    /// Return `true` if the frame is finished, `false` otherwise.
204    /// Return the CPU state.
205    pub fn clock_based_on_video(&mut self, max_clocks: u32) -> (bool, cpu::CpuState) {
206        let mut prev_vblank = self.bus.gpu().in_vblank();
207        let mut current_vblank = prev_vblank;
208
209        let mut clocks = 0;
210
211        while !current_vblank || prev_vblank {
212            let (added_clock, cpu_state) = self.common_clock();
213            if cpu_state != cpu::CpuState::Normal {
214                return (false, cpu_state);
215            } else {
216                clocks += added_clock;
217                if clocks >= max_clocks {
218                    return (false, cpu_state);
219                }
220            }
221
222            prev_vblank = current_vblank;
223            current_vblank = self.bus.gpu().in_vblank();
224        }
225
226        (true, cpu::CpuState::Normal)
227    }
228
229    pub fn clock_full_audio_frame(&mut self) -> cpu::CpuState {
230        // sync the CPU clocks to the SPU so that the audio would be clearer.
231        const CYCLES_PER_FRAME: u32 = 564480;
232
233        let mut clocks = 0;
234        while clocks < CYCLES_PER_FRAME {
235            let (added_clock, cpu_state) = self.common_clock();
236            clocks += added_clock;
237            if cpu_state != cpu::CpuState::Normal {
238                return cpu_state;
239            }
240        }
241
242        cpu::CpuState::Normal
243    }
244
245    pub fn clock_full_video_frame(&mut self) -> cpu::CpuState {
246        let mut prev_vblank = self.bus.gpu().in_vblank();
247        let mut current_vblank = prev_vblank;
248
249        while !current_vblank || prev_vblank {
250            let cpu_state = self.common_clock().1;
251            if cpu_state != cpu::CpuState::Normal {
252                return cpu_state;
253            }
254
255            prev_vblank = current_vblank;
256            current_vblank = self.bus.gpu().in_vblank();
257        }
258
259        cpu::CpuState::Normal
260    }
261
262    pub fn change_controller_key_state(&mut self, key: DigitalControllerKey, pressed: bool) {
263        self.bus
264            .controller_mem_card_mut()
265            .change_controller_key_state(key, pressed);
266    }
267
268    pub fn change_cdrom_shell_open_state(&mut self, open: bool) {
269        self.bus.cdrom_mut().change_cdrom_shell_open_state(open);
270    }
271
272    pub fn blit_to_front(
273        &mut self,
274        dest_image: Arc<Image>,
275        full_vram: bool,
276        in_future: Box<dyn GpuFuture>,
277    ) -> Box<dyn GpuFuture> {
278        self.bus
279            .gpu_mut()
280            .sync_gpu_and_blit_to_front(dest_image, full_vram, in_future)
281    }
282
283    pub fn take_audio_buffer(&mut self) -> Vec<f32> {
284        self.bus.spu_mut().take_audio_buffer()
285    }
286
287    pub fn cpu(&mut self) -> &mut cpu::Cpu {
288        &mut self.cpu
289    }
290
291    pub fn bus_read_u32(&mut self, addr: u32) -> Result<u32> {
292        // make sure its aligned
293        if addr % 4 != 0 {
294            return Err("Unaligned memory access".to_string());
295        }
296        self.bus.read_u32(addr)
297    }
298
299    pub fn bus_read_u16(&mut self, addr: u32) -> Result<u16> {
300        // make sure its aligned
301        if addr % 2 != 0 {
302            return Err("Unaligned memory access".to_string());
303        }
304
305        self.bus.read_u16(addr)
306    }
307
308    pub fn bus_read_u8(&mut self, addr: u32) -> Result<u8> {
309        self.bus.read_u8(addr)
310    }
311
312    pub fn print_spu_state(&self) {
313        self.bus.spu().print_state();
314    }
315}