diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 0be2e940bdddff5ddc9b834fec35d4a08e435ec8..24135de0199c7c0c6b26dc1a87ceaf01cba2469b 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -2,7 +2,7 @@ pub mod audio; pub mod data; -pub mod graphics; +pub mod sdl; use audio::Audio; use boytacean::{ @@ -15,10 +15,15 @@ use boytacean::{ }; use chrono::Utc; use clap::Parser; -use graphics::{surface_from_bytes, Graphics}; use image::ColorType; +use sdl::{surface_from_bytes, SdlSystem}; use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl}; -use std::{cmp::max, path::Path, time::SystemTime}; +use std::{ + cmp::max, + path::Path, + thread, + time::{Duration, Instant, SystemTime}, +}; /// The scale at which the screen is going to be drawn /// meaning the ratio between Game Boy resolution and @@ -56,7 +61,7 @@ pub struct EmulatorOptions { pub struct Emulator { system: GameBoy, auto_mode: bool, - graphics: Option<Graphics>, + sdl: Option<SdlSystem>, audio: Option<Audio>, title: &'static str, rom_path: String, @@ -74,7 +79,7 @@ impl Emulator { Self { system, auto_mode: options.auto_mode, - graphics: None, + sdl: None, audio: None, title: TITLE, rom_path: String::from("invalid"), @@ -176,7 +181,7 @@ impl Emulator { } pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) { - self.graphics = Some(Graphics::new( + self.sdl = Some(SdlSystem::new( sdl, self.title, DISPLAY_WIDTH as u32, @@ -198,12 +203,14 @@ impl Emulator { "========= Cartridge =========\n{}\n=============================", rom ); - self.graphics - .as_mut() - .unwrap() - .window_mut() - .set_title(format!("{} [{}]", self.title, rom.title()).as_str()) - .unwrap(); + match self.sdl { + Some(ref mut sdl) => { + sdl.window_mut() + .set_title(format!("{} [{}]", self.title, rom.title()).as_str()) + .unwrap(); + } + None => (), + } self.rom_path = String::from(path_res); } @@ -250,19 +257,15 @@ impl Emulator { // 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); + self.sdl.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(); + self.sdl.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(); + let texture_creator = self.sdl.as_mut().unwrap().canvas.texture_creator(); // creates the texture streaming that is going to be used // as the target for the pixel buffer @@ -291,7 +294,7 @@ impl Emulator { // 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() { + while let Some(event) = self.sdl.as_mut().unwrap().event_pump.poll_event() { match event { Event::Quit { .. } => break 'main, Event::KeyDown { @@ -351,7 +354,7 @@ impl Emulator { } } - let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); + let current_time = self.sdl.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 @@ -426,11 +429,11 @@ impl Emulator { // 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(); + self.sdl.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 + self.sdl .as_mut() .unwrap() .canvas @@ -439,7 +442,7 @@ impl Emulator { // presents the canvas effectively updating the screen // information presented to the user - self.graphics.as_mut().unwrap().canvas.present(); + self.sdl.as_mut().unwrap().canvas.present(); } // calculates the number of ticks that have elapsed since the @@ -461,15 +464,88 @@ impl Emulator { self.next_tick_time_i = self.next_tick_time.ceil() as u32; } - let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); + let current_time = self.sdl.as_mut().unwrap().timer_subsystem.ticks(); let pending_time = self.next_tick_time_i.saturating_sub(current_time); - self.graphics + self.sdl .as_mut() .unwrap() .timer_subsystem .delay(pending_time); } } + + pub fn run_headless(&mut self) { + // 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; + + let reference = Instant::now(); + + // the main loop to execute the multiple machine clocks, in + // theory the emulator should keep an infinite loop here + loop { + // increments the counter that will keep track + // on the number of visual ticks since beginning + counter = counter.wrapping_add(1); + + let current_time = reference.elapsed().as_millis() as u32; + + if current_time >= self.next_tick_time_i { + // re-starts the counter cycles with the number of pending cycles + // from the previous tick + let mut counter_cycles = pending_cycles; + + // 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, this operation also + // takes into account the current Game Boy speed multiplier (GBC) + let cycle_limit = (self.logic_frequency as f32 * self.system.multiplier() 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; + break; + } + + // 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 += self.system.clock() as u32; + } + + // 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 = reference.elapsed().as_millis() as u32; + let pending_time = self.next_tick_time_i.saturating_sub(current_time); + let ten_millis = Duration::from_millis(pending_time as u64); + thread::sleep(ten_millis); + } + } } #[derive(Parser, Debug)] @@ -540,7 +616,11 @@ fn main() { emulator.start(SCREEN_SCALE); emulator.load_rom(Some(&args.rom_path)); emulator.toggle_palette(); - emulator.run(); + if args.headless { + emulator.run_headless(); + } else { + emulator.run(); + } } fn build_device(device: &str) -> Box<dyn SerialDevice> { diff --git a/frontends/sdl/src/graphics.rs b/frontends/sdl/src/sdl.rs similarity index 96% rename from frontends/sdl/src/graphics.rs rename to frontends/sdl/src/sdl.rs index 62941ddb9b1af3fa63681236347acfa921b68c50..add0f6708406838b017be121bc516eac375ddb68 100644 --- a/frontends/sdl/src/graphics.rs +++ b/frontends/sdl/src/sdl.rs @@ -3,10 +3,10 @@ use sdl2::{ AudioSubsystem, EventPump, Sdl, TimerSubsystem, VideoSubsystem, }; -/// Structure that provides the complete set of Graphics +/// Structure that provides the complete set of SDL Graphics /// and Sound syb-system ready to be used by the overall /// emulator infrastructure. -pub struct Graphics { +pub struct SdlSystem { pub canvas: Canvas<Window>, pub video_subsystem: VideoSubsystem, pub timer_subsystem: TimerSubsystem, @@ -15,7 +15,7 @@ pub struct Graphics { pub ttf_context: Sdl2TtfContext, } -impl Graphics { +impl SdlSystem { /// Start the SDL sub-system and all of its structure and returns /// a structure with all the needed stuff to handle SDL graphics /// and sound.