diff --git a/.gitignore b/.gitignore index c093af014652307f1bb2fc0f0a4a4d3c66d51132..b61206a4a5e2b58be1c92cfe751177e41a387591 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +*.pyd .DS_Store @@ -6,6 +7,8 @@ Cargo.lock /.vscode/settings.json +/.eggs +/.venv /.idea /ndk diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 40733f41dfc9d5e6c85dd7413bc84ab28e725546..9b52b50d959e6f0514e5985f51ee0341fd4453b2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -79,6 +79,17 @@ test-rust: - rustc --version - cargo test +test-pyo3: + stage: test + parallel: + matrix: + - RUST_VERSION: ["1.74.0"] + script: + - rustup toolchain install $RUST_VERSION + - rustup override set $RUST_VERSION + - rustc --version + - python setup.py test + deploy-netlify-preview: stage: deploy script: diff --git a/README.md b/README.md index cf6459a7b91135c31a4f9c199049aaa4548ed9a2..044f1c28ccccf783699497713f8a6448e3be5254 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ cargo build pip install . ``` +or + +```bash +python setup.py install +``` + ### WASM for Node.js ```bash diff --git a/frontends/libretro/src/lib.rs b/frontends/libretro/src/lib.rs index cb8d838fce9cf50f6eafd31e3293570cbbc4e8d8..c1540645f8b814c5e03dd0e7bb467df9d9a2550b 100644 --- a/frontends/libretro/src/lib.rs +++ b/frontends/libretro/src/lib.rs @@ -385,7 +385,7 @@ pub unsafe extern "C" fn retro_load_game(game: *const RetroGameInfo) -> bool { instance.set_mode(mode); instance.reset(); instance.load(true); - instance.load_cartridge(rom); + instance.load_cartridge(rom).unwrap(); update_vars(); true } diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index f7852aa299139b204b140cb6e19bece953af8ca4..1c073ab5b14fcf2d1e91a89429e79d1d82f7f29d 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -8,6 +8,7 @@ pub mod test; use audio::Audio; use boytacean::{ devices::{printer::PrinterDevice, stdout::StdoutDevice}, + error::Error, gb::{AudioProvider, GameBoy, GameBoyMode}, info::Info, pad::PadKey, @@ -225,7 +226,7 @@ impl Emulator { )); } - pub fn load_rom(&mut self, path: Option<&str>) { + pub fn load_rom(&mut self, path: Option<&str>) -> Result<(), Error> { let rom_path: &str = path.unwrap_or(&self.rom_path); let ram_path = replace_ext(rom_path, "sav").unwrap_or_else(|| "invalid".to_string()); let rom = self.system.load_rom_file( @@ -235,7 +236,7 @@ impl Emulator { } else { None }, - ); + )?; println!( "========= Cartridge =========\n{}\n=============================", rom @@ -253,12 +254,14 @@ impl Emulator { .to_str() .unwrap() .to_string(); + Ok(()) } - pub fn reset(&mut self) { + pub fn reset(&mut self) -> Result<(), Error> { self.system.reset(); self.system.load(true); - self.load_rom(None); + self.load_rom(None)?; + Ok(()) } pub fn apply_cheats(&mut self, cheats: &Vec<String>) { @@ -418,7 +421,7 @@ impl Emulator { Event::KeyDown { keycode: Some(Keycode::R), .. - } => self.reset(), + } => self.reset().unwrap(), Event::KeyDown { keycode: Some(Keycode::B), .. @@ -528,7 +531,7 @@ impl Emulator { } self.system.reset(); self.system.load(true); - self.load_rom(Some(&filename)); + self.load_rom(Some(&filename)).unwrap(); } _ => (), } @@ -989,7 +992,7 @@ fn main() { }; let mut emulator = Emulator::new(game_boy, options); emulator.start(SCREEN_SCALE); - emulator.load_rom(Some(&args.rom_path)); + emulator.load_rom(Some(&args.rom_path)).unwrap(); emulator.apply_cheats(&args.cheats); emulator.toggle_palette(); diff --git a/setup.py b/setup.py index 51b98c155e20cfa25c9120d3f5219e2f4b8db58d..f49970180c57bcf2ca1f41327841f1ea0fa1d4fb 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ setuptools.setup( keywords="gameboy emulator rust", url="https://boytacean.joao.me", packages=["boytacean"], + test_suite="boytacean.test", package_dir={"": os.path.normpath("src/python")}, rust_extensions=[ setuptools_rust.RustExtension( diff --git a/src/gb.rs b/src/gb.rs index 6386b2283d4f2b95f8b95357a6cc10c621ec509e..260828968485b9a31b6631a9ff1978154158653d 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -426,9 +426,20 @@ impl GameBoy { let rom = self.rom().clone(); self.reset(); self.load(true); - self.load_cartridge(rom); + self.load_cartridge(rom).unwrap(); } + /// Advance the clock of the system by one tick, this will + /// usually imply executing one CPU instruction and advancing + /// all the other components of the system by the required + /// amount of cycles. + /// + /// This method takes into account the current speed of the + /// system (single or double) and will execute the required + /// amount of cycles in the other components of the system + /// accordingly. + /// + /// The amount of cycles executed by the CPU is returned. pub fn clock(&mut self) -> u16 { let cycles = self.cpu_clock() as u16; let cycles_n = cycles / self.multiplier() as u16; @@ -1081,12 +1092,16 @@ impl GameBoy { Ok(()) } - pub fn load_cartridge(&mut self, rom: Cartridge) -> &mut Cartridge { + pub fn load_cartridge(&mut self, rom: Cartridge) -> Result<&mut Cartridge, Error> { self.mmu().set_rom(rom); - self.mmu().rom() + Ok(self.mmu().rom()) } - pub fn load_rom(&mut self, data: &[u8], ram_data: Option<&[u8]>) -> &mut Cartridge { + pub fn load_rom( + &mut self, + data: &[u8], + ram_data: Option<&[u8]>, + ) -> Result<&mut Cartridge, Error> { let mut rom = Cartridge::from_data(data); if let Some(ram_data) = ram_data { rom.set_ram_data(ram_data) @@ -1094,11 +1109,15 @@ impl GameBoy { self.load_cartridge(rom) } - pub fn load_rom_file(&mut self, path: &str, ram_path: Option<&str>) -> &mut Cartridge { - let data = read_file(path).unwrap(); + pub fn load_rom_file( + &mut self, + path: &str, + ram_path: Option<&str>, + ) -> Result<&mut Cartridge, Error> { + let data = read_file(path)?; match ram_path { Some(ram_path) => { - let ram_data = read_file(ram_path).unwrap(); + let ram_data = read_file(ram_path)?; self.load_rom(&data, Some(&ram_data)) } None => self.load_rom(&data, None), @@ -1184,12 +1203,12 @@ impl GameBoy { })); } - pub fn load_rom_wa(&mut self, data: &[u8]) -> Cartridge { - let rom = self.load_rom(data, None); + pub fn load_rom_wa(&mut self, data: &[u8]) -> Result<Cartridge, String> { + let rom = self.load_rom(data, None).map_err(|e| e.to_string())?; rom.set_rumble_cb(|active| { rumble_callback(active); }); - rom.clone() + Ok(rom.clone()) } pub fn load_callbacks_wa(&mut self) { diff --git a/src/py.rs b/src/py.rs index 2e948c70aa8be704a8652e40690915c90b5fe23d..a9fb17fbe80e32cde3fafeab3d6e5e71f63a45c7 100644 --- a/src/py.rs +++ b/src/py.rs @@ -46,12 +46,18 @@ impl GameBoy { .map_err(PyErr::new::<PyException, _>) } - pub fn load_rom(&mut self, data: &[u8]) { - self.system.load_rom(data, None); + pub fn load_rom(&mut self, data: &[u8]) -> PyResult<()> { + self.system + .load_rom(data, None) + .map(|_| ()) + .map_err(PyErr::new::<PyException, _>) } - pub fn load_rom_file(&mut self, path: &str) { - self.system.load_rom_file(path, None); + pub fn load_rom_file(&mut self, path: &str) -> PyResult<()> { + self.system + .load_rom_file(path, None) + .map(|_| ()) + .map_err(PyErr::new::<PyException, _>) } pub fn read_memory(&mut self, addr: u16) -> u8 { diff --git a/src/python/boytacean/gb.py b/src/python/boytacean/gb.py index ecf2292cfab7e84d823a30814611a40b02aa5ba5..5e2b3e3308226aaa74d5eeaca038e557e0c9e40d 100644 --- a/src/python/boytacean/gb.py +++ b/src/python/boytacean/gb.py @@ -2,7 +2,11 @@ from enum import Enum from contextlib import contextmanager from typing import Any, Iterable, Union, cast -from PIL.Image import Image, frombytes +try: + from PIL.Image import Image, frombytes +except ImportError: + Image = Any + frombytes = Any from .palettes import PALETTES from .video import VideoCapture diff --git a/src/python/boytacean/test/__init__.py b/src/python/boytacean/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/boytacean/test/base.py b/src/python/boytacean/test/base.py new file mode 100644 index 0000000000000000000000000000000000000000..487f0230675e1aafecc8888c02e01a42f68a29d8 --- /dev/null +++ b/src/python/boytacean/test/base.py @@ -0,0 +1,21 @@ +import unittest + +from os.path import dirname, realpath, join + +from boytacean import GameBoy + + +class BaseTest(unittest.TestCase): + + def test_pocket(self): + CURRENT_DIR = dirname(realpath(__file__)) + ROM_PATH = join(CURRENT_DIR, "../../../../res/roms/demo/pocket.gb") + FRAME_COUNT = 600 + LOAD_GRAPHICS = False + + gb = GameBoy( + apu_enabled=False, serial_enabled=False, load_graphics=LOAD_GRAPHICS + ) + gb.load_rom(ROM_PATH) + for _ in range(FRAME_COUNT): + gb.next_frame() diff --git a/src/test.rs b/src/test.rs index ae82c98b135a2e1dae87966b4348e23adba530c4..6ebd856c258a4fdf40137dcd5eb7ef5e49cada4a 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,6 @@ use crate::{ devices::buffer::BufferDevice, + error::Error, gb::{GameBoy, GameBoyMode}, ppu::FRAME_BUFFER_SIZE, }; @@ -25,12 +26,16 @@ pub fn build_test(options: TestOptions) -> GameBoy { game_boy } -pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> GameBoy { +pub fn run_test( + rom_path: &str, + max_cycles: Option<u64>, + options: TestOptions, +) -> Result<GameBoy, Error> { let mut cycles = 0u64; let max_cycles = max_cycles.unwrap_or(u64::MAX); let mut game_boy = build_test(options); - game_boy.load_rom_file(rom_path, None); + game_boy.load_rom_file(rom_path, None)?; loop { cycles += game_boy.clock() as u64; @@ -39,28 +44,32 @@ pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) - } } - game_boy + Ok(game_boy) } -pub fn run_step_test(rom_path: &str, addr: u16, options: TestOptions) -> GameBoy { +pub fn run_step_test(rom_path: &str, addr: u16, options: TestOptions) -> Result<GameBoy, Error> { let mut game_boy = build_test(options); - game_boy.load_rom_file(rom_path, None); + game_boy.load_rom_file(rom_path, None)?; game_boy.step_to(addr); - game_boy + Ok(game_boy) } -pub fn run_serial_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> String { - let mut game_boy = run_test(rom_path, max_cycles, options); - game_boy.serial().device().state() +pub fn run_serial_test( + rom_path: &str, + max_cycles: Option<u64>, + options: TestOptions, +) -> Result<String, Error> { + let mut game_boy = run_test(rom_path, max_cycles, options)?; + Ok(game_boy.serial().device().state()) } pub fn run_image_test( rom_path: &str, max_cycles: Option<u64>, options: TestOptions, -) -> [u8; FRAME_BUFFER_SIZE] { - let mut game_boy = run_test(rom_path, max_cycles, options); - *game_boy.frame_buffer() +) -> Result<[u8; FRAME_BUFFER_SIZE], Error> { + let mut game_boy = run_test(rom_path, max_cycles, options)?; + Ok(*game_boy.frame_buffer()) } #[cfg(test)] @@ -75,7 +84,9 @@ mod tests { "res/roms/test/blargg/cpu/cpu_instrs.gb", 0x0100, TestOptions::default(), - ); + ) + .unwrap(); + assert_eq!(result.cpu_i().pc(), 0x0100); assert_eq!(result.cpu_i().sp(), 0xfffe); assert_eq!(result.cpu_i().af(), 0x01b0); @@ -96,7 +107,8 @@ mod tests { "res/roms/test/blargg/cpu/cpu_instrs.gb", Some(300000000), TestOptions::default(), - ); + ) + .unwrap(); assert_eq!(result, "cpu_instrs\n\n01:ok 02:ok 03:ok 04:ok 05:ok 06:ok 07:ok 08:ok 09:ok 10:ok 11:ok \n\nPassed all tests\n"); } @@ -106,7 +118,8 @@ mod tests { "res/roms/test/blargg/instr_timing/instr_timing.gb", Some(50000000), TestOptions::default(), - ); + ) + .unwrap(); assert_eq!(result, "instr_timing\n\n\nPassed\n"); } }