diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index b4fbcd30769f944251cd22529c330d8955c3be13..9f95a6a8c6ccb640df366b85d768f8df1d032c66 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -9,17 +9,11 @@ use boytacean::{ ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, }; use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; -use std::time::SystemTime; +use std::{cmp::max, time::SystemTime}; use util::Graphics; use crate::util::surface_from_bytes; -/// The ratio at which the logic of the Game Boy is -/// going to be run, increasing this value will provide -/// better emulator accuracy, please keep in mind that -/// the PPU will keep running at the same speed. -const LOGIC_RATIO: f32 = 2.0; - /// 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. @@ -47,7 +41,8 @@ impl Default for Benchmark { pub struct Emulator { system: GameBoy, graphics: Graphics, - logic_ratio: f32, + logic_frequency: u32, + visual_frequency: f32, next_tick_time: f32, next_tick_time_i: u32, palettes: [PaletteInfo; 3], @@ -64,7 +59,8 @@ impl Emulator { DISPLAY_HEIGHT as u32, screen_scale, ), - logic_ratio: LOGIC_RATIO, + logic_frequency: GameBoy::CPU_FREQ, + visual_frequency: GameBoy::VISUAL_FREQ, next_tick_time: 0.0, next_tick_time_i: 0, palettes: [ @@ -164,6 +160,10 @@ impl Emulator { ) .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; @@ -192,6 +192,14 @@ impl Emulator { keycode: Some(Keycode::P), .. } => self.toggle_palette(), + 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), .. @@ -208,7 +216,6 @@ impl Emulator { self.system.key_lift(key) } } - Event::DropFile { filename, .. } => { self.system.reset(); self.system.load_boot_default(); @@ -220,19 +227,21 @@ impl Emulator { let current_time = self.graphics.timer_subsystem.ticks(); - let mut counter_cycles = 0u32; + let mut counter_cycles = pending_cycles; let mut last_frame = 0xffffu16; if current_time >= self.next_tick_time_i { // calculates the number of cycles that are meant to be the target - // for the current "tick" operation this is basically the number of - // cycles per LCD roundtrip divided by the logic ratio - let cycle_limit = (GameBoy::LCD_CYCLES as f32 / self.logic_ratio) as u32; + // 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; break; } @@ -266,12 +275,22 @@ impl Emulator { } } - let logic_frequency = - GameBoy::CPU_FREQ as f32 / GameBoy::LCD_CYCLES as f32 * self.logic_ratio; + // 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 / logic_frequency; + self.next_tick_time += (1000.0 / self.visual_frequency) * ticks as f32; self.next_tick_time_i = self.next_tick_time.ceil() as u32; } diff --git a/frontends/sdl/src/util.rs b/frontends/sdl/src/util.rs index df6a550f781e486ccdf4b4b914b1b6501ee131c4..8ed5af04a1f5535747765f8bdd3a1550517bf3ad 100644 --- a/frontends/sdl/src/util.rs +++ b/frontends/sdl/src/util.rs @@ -44,12 +44,7 @@ impl Graphics { // creates an accelerated canvas to be used in the drawing // then clears it so that is can be presented empty initially - let mut canvas = window - .into_canvas() - .accelerated() - .present_vsync() - .build() - .unwrap(); + let mut canvas = window.into_canvas().accelerated().build().unwrap(); canvas.set_logical_size(width, height).unwrap(); canvas.clear(); diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts index b954d9472504e12338fccb0dc8b0d44af658bc20..02bb2589dab511c0315c10388444a83bf23d0a16 100644 --- a/frontends/web/ts/gb.ts +++ b/frontends/web/ts/gb.ts @@ -29,9 +29,24 @@ import info from "../package.json"; // eslint-disable-next-line @typescript-eslint/no-explicit-any declare const require: any; +/** + * The frequency at which the Game Boy emulator should + * run "normally". + */ const LOGIC_HZ = 4194304; +/** + * The frequency at witch the the visual loop is going to + * run, increasing this value will have a consequence in + * the visual frames per second (FPS) of emulation. + */ const VISUAL_HZ = 59.7275; + +/** + * The frequency of the pause polling update operation, + * increasing this value will make resume from emulation + * paused state fasted. + */ const IDLE_HZ = 10; const DISPLAY_WIDTH = 160; @@ -215,16 +230,6 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // reached the flush of the "tick" logic is skipped if (currentTime < this.nextTickTime) return pending; - // 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 - if (this.nextTickTime === 0) this.nextTickTime = currentTime; - let ticks = Math.ceil( - (currentTime - this.nextTickTime) / - ((1 / this.visualFrequency) * 1000) - ); - ticks = Math.max(ticks, 1); - // initializes the counter of cycles with the pending number // of cycles coming from the previous tick let counterCycles = pending; @@ -287,8 +292,21 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.frameStart = currentTime; } - // updates the next update time reference to the, so that it - // can be used to control the game loop + // 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 (this.nextTickTime === 0) this.nextTickTime = currentTime; + let ticks = Math.ceil( + (currentTime - this.nextTickTime) / + ((1 / this.visualFrequency) * 1000) + ); + ticks = Math.max(ticks, 1); + + // updates the next update time according to the number of ticks + // that have elapsed since the last operation, this way this value + // can better be used to control the game loop this.nextTickTime += (1000 / this.visualFrequency) * ticks; // calculates the new number of pending (overflow) cycles diff --git a/src/gb.rs b/src/gb.rs index 29c29bf0a271760f371db97b8855b0e75928070d..f78ff75fc82a66905b601237ed687a42cb9e29fa 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -219,10 +219,14 @@ impl GameBoy { /// Gameboy implementations that are meant with performance /// in mind and that do not support WASM interface of copy. impl GameBoy { - /// The logical frequency of the Game Boy + /// The logic frequency of the Game Boy /// CPU in hz. pub const CPU_FREQ: u32 = 4194304; + /// The visual frequency (refresh rate) + /// of the Game Boy, close to 60 hz. + pub const VISUAL_FREQ: f32 = 59.7275; + /// The cycles taken to run a complete frame /// loop in the Game Boy's PPU (in CPU cycles). pub const LCD_CYCLES: u32 = 70224;