From 04d2edfc01247288cdc55d84378be50edba499bf Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Thu, 5 Dec 2019 01:15:49 +0200 Subject: [PATCH] Refactor "backend" concept See plat/sdl2 Former-commit-id: 5f7c9625467003d5b73307959095eb5365db523e --- Cargo.toml | 4 ++ src/bin/main.rs | 79 ---------------------------------- src/core/cartridge.rs | 41 +++++++++++------- src/core/gba.rs | 24 +++-------- src/debugger/command.rs | 51 +++++++++++----------- src/lib.rs | 22 +++++++++- src/plat/sdl2/audio.rs | 1 + src/{bin => plat/sdl2}/cli.yml | 10 +---- src/plat/sdl2/keyboard.rs | 64 +++++++++++++++++++++++++++ src/plat/sdl2/main.rs | 78 +++++++++++++++++++++++++++++++++ src/plat/sdl2/video.rs | 77 +++++++++++++++++++++++++++++++++ src/util.rs | 32 +++++++++++++- 12 files changed, 336 insertions(+), 147 deletions(-) delete mode 100644 src/bin/main.rs create mode 100644 src/plat/sdl2/audio.rs rename src/{bin => plat/sdl2}/cli.yml (78%) create mode 100644 src/plat/sdl2/keyboard.rs create mode 100644 src/plat/sdl2/main.rs create mode 100644 src/plat/sdl2/video.rs diff --git a/Cargo.toml b/Cargo.toml index 0ac1d2e..055e629 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,10 @@ zip = "0.5.3" ctrlc = "3.1.3" cpal="0.10.0" +[[bin]] +name = "rba-sdl2" +path = "src/plat/sdl2/main.rs" + [profile.dev] opt-level = 0 debug = true \ No newline at end of file diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index 3aba732..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::ffi::OsStr; -use std::time; - -#[macro_use] -extern crate clap; - -use clap::{App, ArgMatches}; - -extern crate rustboyadvance_ng; - -use rustboyadvance_ng::backend::*; -use rustboyadvance_ng::core::arm7tdmi::Core; -use rustboyadvance_ng::core::cartridge::Cartridge; -use rustboyadvance_ng::core::{GBAError, GBAResult, GameBoyAdvance}; -use rustboyadvance_ng::debugger::Debugger; -use rustboyadvance_ng::util::read_bin_file; - -fn run_emulator(matches: &ArgMatches) -> GBAResult<()> { - let skip_bios = matches.occurrences_of("skip_bios") != 0; - let no_framerate_limit = matches.occurrences_of("no_framerate_limit") != 0; - let debug = matches.occurrences_of("debug") != 0; - - let backend: Box = match matches.value_of("backend") { - Some("sdl2") => Box::new(Sdl2Backend::new()), - Some("minifb") => Box::new(MinifbBackend::new()), - // None => DummyBackend::new(), - None => Box::new(DummyBackend::new()), - _ => unreachable!(), - }; - - let bios_bin = read_bin_file(matches.value_of("bios").unwrap_or_default())?; - - let cart = Cartridge::from_path(matches.value_of("game_rom").unwrap())?; - println!("loaded rom: {:#?}", cart.header); - - let mut core = Core::new(); - if skip_bios { - core.skip_bios(); - } - - let mut gba = GameBoyAdvance::new(core, bios_bin, cart, backend); - - if debug { - gba.cpu.set_verbose(true); - let mut debugger = Debugger::new(gba); - println!("starting debugger..."); - debugger.repl(matches.value_of("script_file"))?; - println!("ending debugger..."); - } else { - let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60); - loop { - let start_time = time::Instant::now(); - gba.frame(); - if !no_framerate_limit { - let time_passed = start_time.elapsed(); - let delay = frame_time.checked_sub(time_passed); - match delay { - None => {} - Some(delay) => { - ::std::thread::sleep(delay); - } - }; - } - } - } - - Ok(()) -} - -fn main() { - let yaml = load_yaml!("cli.yml"); - let matches = App::from_yaml(yaml).get_matches(); - - let result = run_emulator(&matches); - - if let Err(err) = result { - println!("Got an error: {:?}", err); - } -} diff --git a/src/core/cartridge.rs b/src/core/cartridge.rs index 4004d36..06e3da3 100644 --- a/src/core/cartridge.rs +++ b/src/core/cartridge.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::fs::File; use std::io::prelude::*; use std::path::Path; @@ -8,7 +9,7 @@ use zip::ZipArchive; use super::super::util::read_bin_file; use super::arm7tdmi::{bus::Bus, Addr}; -use super::GBAResult; +use super::{GBAError, GBAResult}; /// From GBATEK /// @@ -75,30 +76,40 @@ pub struct Cartridge { bytes: Box<[u8]>, } -fn load_rom(path: &str) -> GBAResult> { - if path.ends_with(".zip") { - let zipfile = File::open(path)?; - let mut archive = ZipArchive::new(zipfile)?; - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - if file.name().ends_with(".gba") { - let mut buf = Vec::new(); - file.read_to_end(&mut buf)?; +fn load_rom(path: &Path) -> GBAResult> { + match path.extension() { + Some(extension) => match extension.to_str() { + Some("zip") => { + let zipfile = File::open(path)?; + let mut archive = ZipArchive::new(zipfile)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + if file.name().ends_with(".gba") { + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + return Ok(buf); + } + } + panic!("no .gba file contained in the zip file"); + } + _ => { + let buf = read_bin_file(path)?; return Ok(buf); } + }, + _ => { + let buf = read_bin_file(path)?; + return Ok(buf); } - panic!("no .gba file contained in the zip file"); - } else { - let buf = read_bin_file(path)?; - Ok(buf) } } impl Cartridge { const MIN_SIZE: usize = 4 * 1024 * 1024; - pub fn from_path(rom_path: &str) -> GBAResult { + pub fn from_path(rom_path: &Path) -> GBAResult { let mut rom_bin = load_rom(rom_path)?; + println!("loaded {} bytes", rom_bin.len()); if rom_bin.len() < Cartridge::MIN_SIZE { rom_bin.resize_with(Cartridge::MIN_SIZE, Default::default); diff --git a/src/core/gba.rs b/src/core/gba.rs index 7b42cc0..0d69eb6 100644 --- a/src/core/gba.rs +++ b/src/core/gba.rs @@ -7,45 +7,34 @@ use super::iodev::*; use super::sysbus::SysBus; use super::SyncedIoDevice; -use crate::backend::*; + +use super::super::{AudioInterface, InputInterface, VideoInterface}; pub struct GameBoyAdvance { - backend: Box, pub cpu: Core, pub sysbus: Box, } impl GameBoyAdvance { - pub fn new( - cpu: Core, - bios_rom: Vec, - gamepak: Cartridge, - // TODO rename this "graphics backend" - // TODO add sound backend - backend: Box, - ) -> GameBoyAdvance { + pub fn new(cpu: Core, bios_rom: Vec, gamepak: Cartridge) -> GameBoyAdvance { let io = IoDevices::new(); GameBoyAdvance { - backend: backend, cpu: cpu, sysbus: Box::new(SysBus::new(io, bios_rom, gamepak)), } } - pub fn frame(&mut self) { - self.update_key_state(); + pub fn frame(&mut self, video: &mut VideoInterface, input: &mut InputInterface) { + self.sysbus.io.keyinput = input.poll(); while self.sysbus.io.gpu.state != GpuState::VBlank { self.step_new(); } + video.render(self.sysbus.io.gpu.get_framebuffer()); while self.sysbus.io.gpu.state == GpuState::VBlank { self.step_new(); } } - pub fn update_key_state(&mut self) { - self.sysbus.io.keyinput = self.backend.get_key_state(); - } - pub fn add_breakpoint(&mut self, addr: u32) -> Option { if !self.cpu.breakpoints.contains(&addr) { let new_index = self.cpu.breakpoints.len(); @@ -100,7 +89,6 @@ impl GameBoyAdvance { if let Some(new_gpu_state) = io.gpu.step(cycles, &mut self.sysbus, &mut irqs) { match new_gpu_state { GpuState::VBlank => { - self.backend.render(io.gpu.get_framebuffer()); io.dmac.notify_vblank(); } GpuState::HBlank => io.dmac.notify_hblank(), diff --git a/src/debugger/command.rs b/src/debugger/command.rs index 0146563..0bcca39 100644 --- a/src/debugger/command.rs +++ b/src/debugger/command.rs @@ -102,31 +102,31 @@ impl Debugger { } println!("{}\n", self.gba.cpu); } - Continue => { - self.ctrlc_flag.store(true, Ordering::SeqCst); - while self.ctrlc_flag.load(Ordering::SeqCst) { - let start_time = time::Instant::now(); - self.gba.update_key_state(); - match self.gba.check_breakpoint() { - Some(addr) => { - println!("Breakpoint reached! @{:x}", addr); - break; - } - _ => { - self.gba.step_new(); - } - } - } - } - Frame(count) => { - use super::time::PreciseTime; - let start = PreciseTime::now(); - for _ in 0..count { - self.gba.frame(); - } - let end = PreciseTime::now(); - println!("that took {} seconds", start.to(end)); - } + // Continue => { + // self.ctrlc_flag.store(true, Ordering::SeqCst); + // while self.ctrlc_flag.load(Ordering::SeqCst) { + // let start_time = time::Instant::now(); + // self.gba.update_key_state(); + // match self.gba.check_breakpoint() { + // Some(addr) => { + // println!("Breakpoint reached! @{:x}", addr); + // break; + // } + // _ => { + // self.gba.step_new(); + // } + // } + // } + // } + // Frame(count) => { + // use super::time::PreciseTime; + // let start = PreciseTime::now(); + // for _ in 0..count { + // self.gba.frame(); + // } + // let end = PreciseTime::now(); + // println!("that took {} seconds", start.to(end)); + // } HexDump(addr, nbytes) => { let bytes = self.gba.sysbus.get_bytes(addr..addr + nbytes); hexdump::hexdump(&bytes); @@ -206,6 +206,7 @@ impl Debugger { self.gba.sysbus.io.timers.trace = !self.gba.sysbus.io.timers.trace; } } + _ => println!("Not Implemented",), } } diff --git a/src/lib.rs b/src/lib.rs index 148b9f5..3c5057d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,27 @@ extern crate zip; #[macro_use] pub mod util; -pub mod backend; pub mod core; pub mod debugger; pub mod disass; + +pub trait VideoInterface { + fn render(&mut self, buffer: &[u32]); +} + +pub trait AudioInterface { + fn get_sample_rate(&self); +} + +pub trait InputInterface { + fn poll(&mut self) -> u16; +} + +pub mod prelude { + pub use super::core::arm7tdmi; + pub use super::core::cartridge::Cartridge; + pub use super::core::{GBAError, GBAResult, GameBoyAdvance}; + pub use super::debugger::Debugger; + pub use super::util::read_bin_file; + pub use super::{AudioInterface, InputInterface, VideoInterface}; +} diff --git a/src/plat/sdl2/audio.rs b/src/plat/sdl2/audio.rs new file mode 100644 index 0000000..f3951b9 --- /dev/null +++ b/src/plat/sdl2/audio.rs @@ -0,0 +1 @@ +const SAMPLE_RATE: u32 = 44100; diff --git a/src/bin/cli.yml b/src/plat/sdl2/cli.yml similarity index 78% rename from src/bin/cli.yml rename to src/plat/sdl2/cli.yml index 3214dcf..b195de1 100644 --- a/src/bin/cli.yml +++ b/src/plat/sdl2/cli.yml @@ -1,6 +1,6 @@ -name: rustboyadvance-ng +name: rba-sdl2 author: Michel Heily -about: Game boy advance emulator and debugger +about: RustBoyAdvance SDL2 port args: - bios: help: Sets the bios file to use @@ -19,12 +19,6 @@ args: - no_framerate_limit: long: no-framerate-limit help: Run without frame limiter - - backend: - long: backend - takes_value: true - possible_values: [ sdl2, minifb ] - default_value: minifb - required: false - debug: long: debug help: Start with the debugger attached diff --git a/src/plat/sdl2/keyboard.rs b/src/plat/sdl2/keyboard.rs new file mode 100644 index 0000000..ebff4a7 --- /dev/null +++ b/src/plat/sdl2/keyboard.rs @@ -0,0 +1,64 @@ +use sdl2::keyboard::Keycode; +use sdl2::{event::Event, EventPump}; + +use rustboyadvance_ng::core::keypad as gba_keypad; +use rustboyadvance_ng::InputInterface; + +extern crate bit; +use bit::BitIndex; + +pub struct PlatformSdl2_Keyboard { + event_pump: EventPump, + keyinput: u16, +} + +impl InputInterface for PlatformSdl2_Keyboard { + fn poll(&mut self) -> u16 { + for event in self.event_pump.poll_iter() { + match event { + Event::KeyDown { + keycode: Some(keycode), + .. + } => { + if let Some(key) = keycode_to_keypad(keycode) { + self.keyinput.set_bit(key as usize, false); + } + } + Event::KeyUp { + keycode: Some(keycode), + .. + } => { + if let Some(key) = keycode_to_keypad(keycode) { + self.keyinput.set_bit(key as usize, true); + } + } + Event::Quit { .. } => panic!("quit!"), + _ => {} + } + } + self.keyinput + } +} + +fn keycode_to_keypad(keycode: Keycode) -> Option { + match keycode { + Keycode::Up => Some(gba_keypad::Keys::Up), + Keycode::Down => Some(gba_keypad::Keys::Down), + Keycode::Left => Some(gba_keypad::Keys::Left), + Keycode::Right => Some(gba_keypad::Keys::Right), + Keycode::Z => Some(gba_keypad::Keys::ButtonB), + Keycode::X => Some(gba_keypad::Keys::ButtonA), + Keycode::Return => Some(gba_keypad::Keys::Start), + Keycode::Space => Some(gba_keypad::Keys::Select), + Keycode::A => Some(gba_keypad::Keys::ButtonL), + Keycode::S => Some(gba_keypad::Keys::ButtonR), + _ => None, + } +} + +pub fn create_keyboard(sdl: &sdl2::Sdl) -> PlatformSdl2_Keyboard { + PlatformSdl2_Keyboard { + event_pump: sdl.event_pump().unwrap(), + keyinput: gba_keypad::KEYINPUT_ALL_RELEASED, + } +} diff --git a/src/plat/sdl2/main.rs b/src/plat/sdl2/main.rs new file mode 100644 index 0000000..32f1c7d --- /dev/null +++ b/src/plat/sdl2/main.rs @@ -0,0 +1,78 @@ +extern crate sdl2; +use sdl2::Sdl; + +use std::path::Path; +use std::time; + +#[macro_use] +extern crate clap; + +mod audio; +mod keyboard; +mod video; + +use keyboard::{create_keyboard, PlatformSdl2_Keyboard}; +use video::{create_video_interface, PlatformSdl2_VideoInterface}; + +#[macro_use] +extern crate rustboyadvance_ng; +use rustboyadvance_ng::prelude::*; +use rustboyadvance_ng::util::FpsCounter; + +fn main() { + let yaml = load_yaml!("cli.yml"); + let matches = clap::App::from_yaml(yaml).get_matches(); + + let skip_bios = matches.occurrences_of("skip_bios") != 0; + let no_framerate_limit = matches.occurrences_of("no_framerate_limit") != 0; + let debug = matches.occurrences_of("debug") != 0; + + let bios_path = Path::new(matches.value_of("bios").unwrap_or_default()); + let rom_path = Path::new(matches.value_of("game_rom").unwrap()); + let rom_name = rom_path.file_name().unwrap().to_str().unwrap(); + + let bios_bin = read_bin_file(bios_path).unwrap(); + let cart = Cartridge::from_path(rom_path).unwrap(); + let mut cpu = arm7tdmi::Core::new(); + if skip_bios { + cpu.skip_bios(); + } + let cpu = cpu; + + let sdl_context = sdl2::init().unwrap(); + let mut video = create_video_interface(&sdl_context); + let mut keyboard = create_keyboard(&sdl_context); + + let mut fps_counter = FpsCounter::default(); + let mut gba = GameBoyAdvance::new(cpu, bios_bin, cart); + + if debug { + gba.cpu.set_verbose(true); + let mut debugger = Debugger::new(gba); + println!("starting debugger..."); + debugger.repl(matches.value_of("script_file")).unwrap(); + println!("ending debugger..."); + } else { + let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60); + loop { + let start_time = time::Instant::now(); + + gba.frame(&mut video, &mut keyboard); + if let Some(fps) = fps_counter.tick() { + let title = format!("{} ({} fps)", rom_name, fps); + video.set_window_title(&title); + } + + if !no_framerate_limit { + let time_passed = start_time.elapsed(); + let delay = frame_time.checked_sub(time_passed); + match delay { + None => {} + Some(delay) => { + ::std::thread::sleep(delay); + } + }; + } + } + } +} diff --git a/src/plat/sdl2/video.rs b/src/plat/sdl2/video.rs new file mode 100644 index 0000000..db87b61 --- /dev/null +++ b/src/plat/sdl2/video.rs @@ -0,0 +1,77 @@ +use sdl2::pixels::{Color, PixelFormatEnum}; +use sdl2::rect::{Point, Rect}; +use sdl2::render::{TextureCreator, WindowCanvas}; +use sdl2::video::WindowContext; +use sdl2::Sdl; + +use rustboyadvance_ng::core::gpu::{DISPLAY_HEIGHT, DISPLAY_WIDTH}; +use rustboyadvance_ng::util::FpsCounter; +use rustboyadvance_ng::VideoInterface; + +const SCREEN_WIDTH: u32 = DISPLAY_WIDTH as u32; +const SCREEN_HEIGHT: u32 = DISPLAY_HEIGHT as u32; +const SCALE: u32 = 3; // TODO control via CLI & support window resize + +pub struct PlatformSdl2_VideoInterface { + tc: TextureCreator, + canvas: WindowCanvas, + fps_counter: FpsCounter, +} + +impl PlatformSdl2_VideoInterface { + pub fn set_window_title(&mut self, title: &str) { + self.canvas.window_mut().set_title(&title); + } +} + +impl VideoInterface for PlatformSdl2_VideoInterface { + fn render(&mut self, buffer: &[u32]) { + let mut texture = self + .tc + .create_texture_target(PixelFormatEnum::RGB24, SCREEN_WIDTH, SCREEN_HEIGHT) + .unwrap(); + self.canvas + .with_texture_canvas(&mut texture, |texture_canvas| { + for y in 0i32..(SCREEN_HEIGHT as i32) { + for x in 0i32..(SCREEN_WIDTH as i32) { + let c = buffer[index2d!(x, y, SCREEN_WIDTH as i32) as usize]; + let color = Color::RGB((c >> 16) as u8, (c >> 8) as u8, c as u8); + texture_canvas.set_draw_color(color); + let _ = texture_canvas.draw_point(Point::from((x, y))); + } + } + }) + .unwrap(); + self.canvas + .copy( + &texture, + None, + Some(Rect::new(0, 0, SCREEN_WIDTH * SCALE, SCREEN_HEIGHT * SCALE)), + ) + .unwrap(); + self.canvas.present(); + } +} + +pub fn create_video_interface(sdl: &Sdl) -> PlatformSdl2_VideoInterface { + let video_subsystem = sdl.video().unwrap(); + let window = video_subsystem + .window( + "RustBoyAdvance", + SCREEN_WIDTH * SCALE, + SCREEN_HEIGHT * SCALE, + ) + .opengl() + .position_centered() + .build() + .unwrap(); + let mut canvas = window.into_canvas().accelerated().build().unwrap(); + canvas.set_draw_color(Color::RGB(0, 0, 0)); + canvas.clear(); + let tc = canvas.texture_creator(); + PlatformSdl2_VideoInterface { + tc: tc, + canvas: canvas, + fps_counter: Default::default(), + } +} diff --git a/src/util.rs b/src/util.rs index 0d6178f..bc5732b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,14 +1,44 @@ use std::fs::File; use std::io; use std::io::prelude::*; +use std::path::Path; +use std::time; -pub fn read_bin_file(filename: &str) -> io::Result> { +pub fn read_bin_file(filename: &Path) -> io::Result> { let mut buf = Vec::new(); let mut file = File::open(filename)?; file.read_to_end(&mut buf)?; Ok(buf) } +pub struct FpsCounter { + count: u32, + timer: time::Instant, +} + +impl Default for FpsCounter { + fn default() -> FpsCounter { + FpsCounter { + count: 0, + timer: time::Instant::now(), + } + } +} + +impl FpsCounter { + pub fn tick(&mut self) -> Option { + self.count += 1; + if self.timer.elapsed() >= time::Duration::from_secs(1) { + let fps = self.count; + self.timer = time::Instant::now(); + self.count = 0; + Some(fps) + } else { + None + } + } +} + #[macro_export] macro_rules! index2d { ($x:expr, $y:expr, $w:expr) => {