Newer
Older
#![allow(clippy::uninlined_format_args)]
ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
use graphics::{surface_from_bytes, Graphics};
use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl};
use std::{
cmp::max,
sync::{Arc, Mutex},
time::SystemTime,
};
/// The scale at which the screen is going to be drawn
/// meaning the ratio between Game Boy resolution and
/// the window size to be displayed.
const SCREEN_SCALE: f32 = 2.0;
/// The base title to be used in the window.
pub struct Benchmark {
count: usize,
}
impl Benchmark {
pub fn new(count: usize) -> Self {
}
}
impl Default for Benchmark {
fn default() -> Self {
Self::new(50000000)
}
}
system: Arc<Mutex<Box<GameBoy>>>,
graphics: Option<Graphics>,
audio: Option<Audio>,
logic_frequency: u32,
visual_frequency: f32,
next_tick_time: f32,
next_tick_time_i: u32,
features: Vec<&'static str>,
palettes: [PaletteInfo; 3],
palette_index: usize,
pub fn new(system: Arc<Mutex<Box<GameBoy>>>) -> Self {
graphics: None,
audio: None,
logic_frequency: GameBoy::CPU_FREQ,
visual_frequency: GameBoy::VISUAL_FREQ * 2.0,
next_tick_time: 0.0,
next_tick_time_i: 0,
features: vec!["no-vsync"],
palettes: [
PaletteInfo::new(
"basic",
[
[0xff, 0xff, 0xff],
[0xc0, 0xc0, 0xc0],
[0x60, 0x60, 0x60],
[0x00, 0x00, 0x00],
],
),
PaletteInfo::new(
"hogwards",
[
[0xb6, 0xa5, 0x71],
[0x8b, 0x7e, 0x56],
[0x55, 0x4d, 0x35],
[0x20, 0x1d, 0x13],
],
),
PaletteInfo::new(
"christmas",
[
[0xe8, 0xe7, 0xdf],
[0x8b, 0xab, 0x95],
[0x9e, 0x5c, 0x5e],
[0x53, 0x4d, 0x57],
],
),
],
palette_index: 0,
pub fn start(&mut self, screen_scale: f32, audio_provider: Arc<Mutex<Box<GameBoy>>>) {
let sdl = sdl2::init().unwrap();
self.start_graphics(&sdl, screen_scale);
self.start_audio(&sdl, audio_provider);
}
pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) {
self.graphics = Some(Graphics::new(
&sdl,
TITLE,
DISPLAY_WIDTH as u32,
DISPLAY_HEIGHT as u32,
screen_scale,
!self.features.contains(&"no-accelerated"),
!self.features.contains(&"no-vsync"),
));
}
pub fn start_audio(&mut self, sdl: &Sdl, audio_provider: Arc<Mutex<Box<GameBoy>>>) {
self.audio = Some(Audio::new(sdl, audio_provider));
}
pub fn load_rom(&mut self, path: &str) {
let mut system = self.system.lock().unwrap();
let rom = system.load_rom_file(path);
"========= Cartridge =========\n{}\n=============================",
.set_title(format!("{} [{}]", TITLE, rom.title()).as_str())
pub fn benchmark(&mut self, params: Benchmark) {
println!("Going to run benchmark...");
let mut cycles = 0;
let initial = SystemTime::now();
for _ in 0..count {
cycles += self.system.lock().unwrap().clock() as u32;
}
let delta = initial.elapsed().unwrap().as_millis() as f32 / 1000.0;
let frequency_mhz = cycles as f32 / delta / 1000.0 / 1000.0;
println!(
"Took {:.2} seconds to run {} ticks ({:.2} Mhz)!",
delta, count, frequency_mhz
);
pub fn toggle_palette(&mut self) {
.ppu()
.set_palette_colors(self.palettes[self.palette_index].colors());
self.palette_index = (self.palette_index + 1) % self.palettes.len();
pub fn run(&mut self) {
// updates the icon of the window to reflect the image
// and style of the emulator
let surface = surface_from_bytes(&data::ICON);
self.graphics
.as_mut()
.unwrap()
.window_mut()
.set_icon(&surface);
// creates an accelerated canvas to be used in the drawing
// then clears it and presents it
self.graphics.as_mut().unwrap().canvas.present();
// creates a texture creator for the current canvas, required
// for the creation of dynamic and static textures
let texture_creator = self.graphics.as_mut().unwrap().canvas.texture_creator();
// creates the texture streaming that is going to be used
// as the target for the pixel buffer
let mut texture = texture_creator
.create_texture_streaming(
PixelFormatEnum::RGB24,
DISPLAY_WIDTH as u32,
DISPLAY_HEIGHT as u32,
)
.unwrap();
// starts the variable that will control the number of cycles that
// are going to move (because of overflow) from one tick to another
let mut pending_cycles = 0u32;
// allocates space for the loop ticks counter to be used in each
// iteration cycle
let mut counter = 0u32;
// the main loop to execute the multiple machine clocks, in
// theory the emulator should keep an infinite loop here
'main: loop {
// increments the counter that will keep track
// on the number of visual ticks since beginning
counter = counter.wrapping_add(1);
// obtains an event from the SDL sub-system to be
// processed under the current emulation context
while let Some(event) = self.graphics.as_mut().unwrap().event_pump.poll_event() {
match event {
Event::Quit { .. } => break 'main,
Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => break 'main,
Event::KeyDown {
keycode: Some(Keycode::B),
..
} => self.benchmark(Benchmark::default()),
Event::KeyDown {
keycode: Some(Keycode::P),
..
Event::KeyDown {
keycode: Some(Keycode::Plus),
..
} => self.logic_frequency = self.logic_frequency.saturating_add(400000),
Event::KeyDown {
keycode: Some(Keycode::Minus),
..
} => self.logic_frequency = self.logic_frequency.saturating_sub(400000),
Event::KeyDown {
keycode: Some(keycode),
..
} => {
if let Some(key) = key_to_pad(keycode) {
self.system.lock().unwrap().key_press(key)
Event::KeyUp {
keycode: Some(keycode),
..
} => {
if let Some(key) = key_to_pad(keycode) {
self.system.lock().unwrap().key_lift(key)
Event::DropFile { filename, .. } => {
self.system.lock().unwrap().reset();
self.system.lock().unwrap().load_boot_default();
let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks();
if current_time >= self.next_tick_time_i {
// re-starts the counter cycles with the number of pending cycles
// from the previous tick and the last frame with a dummy value
// meant to be overridden in case there's at least one new frame
// being drawn in the current tick
let mut counter_cycles = pending_cycles;
let mut last_frame = 0xffffu16;
// calculates the number of cycles that are meant to be the target
// for the current "tick" operation this is basically the current
// logic frequency divided by the visual one
let cycle_limit =
(self.logic_frequency as f32 / self.visual_frequency).round() as u32;
loop {
// limits the number of ticks to the typical number
// of cycles expected for the current logic cycle
if counter_cycles >= cycle_limit {
pending_cycles = counter_cycles - cycle_limit;
// obtains a locked reference to the system that is going to be
// valid under the current block
let mut system = self.system.lock().unwrap();
// runs the Game Boy clock, this operation should
// include the advance of both the CPU, PPU, APU
// and any other frequency based component of the system
counter_cycles += system.clock() as u32;
// in case a V-Blank state has been reached a new frame is available
// then the frame must be pushed into SDL for display
if system.ppu_mode() == PpuMode::VBlank && system.ppu_frame() != last_frame
{
// obtains the frame buffer of the Game Boy PPU and uses it
// to update the stream texture, that will latter be copied
// to the canvas
let frame_buffer = system.frame_buffer().as_ref();
texture
.update(None, frame_buffer, DISPLAY_WIDTH * 3)
.unwrap();
// obtains the index of the current PPU frame, this value
// is going to be used to detect for new frame presence
last_frame = system.ppu_frame();
}
// in case there's at least one new frame that was drawn during
// during the current tick, then we need to flush it to the canvas,
// this separation between texture creation and canvas flush prevents
// resources from being over-used in situations where multiple frames
// are generated during the same tick cycle
if last_frame != 0xffffu16 {
// clears the graphics canvas, making sure that no garbage
// pixel data remaining in the pixel buffer, not doing this would
// create visual glitches in OSs like Mac OS X
self.graphics.as_mut().unwrap().canvas.clear();
// copies the texture that was created for the frame (during
// the loop part of the tick) to the canvas
self.graphics
.as_mut()
.unwrap()
.canvas
.copy(&texture, None, None)
.unwrap();
// presents the canvas effectively updating the screen
// information presented to the user
self.graphics.as_mut().unwrap().canvas.present();
// calculates the number of ticks that have elapsed since the
// last draw operation, this is critical to be able to properly
// operate the clock of the CPU in frame drop situations, meaning
// a situation where the system resources are no able to emulate
// the system on time and frames must be skipped (ticks > 1)
if self.next_tick_time == 0.0 {
self.next_tick_time = current_time as f32;
}
let mut ticks = ((current_time as f32 - self.next_tick_time)
/ ((1.0 / self.visual_frequency) * 1000.0))
.ceil() as u8;
ticks = max(ticks, 1);
// updates the next update time reference to the current
// time so that it can be used from game loop control
self.next_tick_time += (1000.0 / self.visual_frequency) * ticks as f32;
self.next_tick_time_i = self.next_tick_time.ceil() as u32;
}
let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks();
let pending_time = self.next_tick_time_i.saturating_sub(current_time);
self.graphics
.as_mut()
.unwrap()
.timer_subsystem
.delay(pending_time);
// creates a new Game Boy instance and loads both the boot ROM
// and the initial game ROM to "start the engine"
let game_boy = Arc::new(Mutex::new(Box::new(GameBoy::new())));
game_boy.try_lock().unwrap().as_mut().load_boot_default();
// creates a new generic emulator structure loads the default
// ROM file and starts running it
let mut emulator = Emulator::new(game_boy);
let game_boy_ref = emulator.system.clone();
emulator.start(SCREEN_SCALE, game_boy_ref);
emulator.load_rom("../../res/roms/pocket.gb");
emulator.toggle_palette();
fn key_to_pad(keycode: Keycode) -> Option<PadKey> {
Keycode::Up => Some(PadKey::Up),
Keycode::Down => Some(PadKey::Down),
Keycode::Left => Some(PadKey::Left),
Keycode::Right => Some(PadKey::Right),
Keycode::Return => Some(PadKey::Start),
Keycode::Return2 => Some(PadKey::Start),
Keycode::Space => Some(PadKey::Select),
Keycode::A => Some(PadKey::A),
Keycode::S => Some(PadKey::B),
_ => None,