Skip to content
Snippets Groups Projects
Verified Commit ae21656b authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

feat: support for increment and decrement CPU

Normalized handling of ticks in SDL according to the Web version implementation.
parent 539564fb
No related branches found
No related tags found
No related merge requests found
Pipeline #2190 passed
...@@ -9,17 +9,11 @@ use boytacean::{ ...@@ -9,17 +9,11 @@ use boytacean::{
ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
}; };
use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum};
use std::time::SystemTime; use std::{cmp::max, time::SystemTime};
use util::Graphics; use util::Graphics;
use crate::util::surface_from_bytes; 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 /// The scale at which the screen is going to be drawn
/// meaning the ratio between Game Boy resolution and /// meaning the ratio between Game Boy resolution and
/// the window size to be displayed. /// the window size to be displayed.
...@@ -47,7 +41,8 @@ impl Default for Benchmark { ...@@ -47,7 +41,8 @@ impl Default for Benchmark {
pub struct Emulator { pub struct Emulator {
system: GameBoy, system: GameBoy,
graphics: Graphics, graphics: Graphics,
logic_ratio: f32, logic_frequency: u32,
visual_frequency: f32,
next_tick_time: f32, next_tick_time: f32,
next_tick_time_i: u32, next_tick_time_i: u32,
palettes: [PaletteInfo; 3], palettes: [PaletteInfo; 3],
...@@ -64,7 +59,8 @@ impl Emulator { ...@@ -64,7 +59,8 @@ impl Emulator {
DISPLAY_HEIGHT as u32, DISPLAY_HEIGHT as u32,
screen_scale, screen_scale,
), ),
logic_ratio: LOGIC_RATIO, logic_frequency: GameBoy::CPU_FREQ,
visual_frequency: GameBoy::VISUAL_FREQ,
next_tick_time: 0.0, next_tick_time: 0.0,
next_tick_time_i: 0, next_tick_time_i: 0,
palettes: [ palettes: [
...@@ -164,6 +160,10 @@ impl Emulator { ...@@ -164,6 +160,10 @@ impl Emulator {
) )
.unwrap(); .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 // allocates space for the loop ticks counter to be used in each
// iteration cycle // iteration cycle
let mut counter = 0u32; let mut counter = 0u32;
...@@ -192,6 +192,14 @@ impl Emulator { ...@@ -192,6 +192,14 @@ impl Emulator {
keycode: Some(Keycode::P), keycode: Some(Keycode::P),
.. ..
} => self.toggle_palette(), } => 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 { Event::KeyDown {
keycode: Some(keycode), keycode: Some(keycode),
.. ..
...@@ -208,7 +216,6 @@ impl Emulator { ...@@ -208,7 +216,6 @@ impl Emulator {
self.system.key_lift(key) self.system.key_lift(key)
} }
} }
Event::DropFile { filename, .. } => { Event::DropFile { filename, .. } => {
self.system.reset(); self.system.reset();
self.system.load_boot_default(); self.system.load_boot_default();
...@@ -220,19 +227,21 @@ impl Emulator { ...@@ -220,19 +227,21 @@ impl Emulator {
let current_time = self.graphics.timer_subsystem.ticks(); let current_time = self.graphics.timer_subsystem.ticks();
let mut counter_cycles = 0u32; let mut counter_cycles = pending_cycles;
let mut last_frame = 0xffffu16; let mut last_frame = 0xffffu16;
if current_time >= self.next_tick_time_i { if current_time >= self.next_tick_time_i {
// calculates the number of cycles that are meant to be the target // calculates the number of cycles that are meant to be the target
// for the current "tick" operation this is basically the number of // for the current "tick" operation this is basically the current
// cycles per LCD roundtrip divided by the logic ratio // logic frequency divided by the visual one
let cycle_limit = (GameBoy::LCD_CYCLES as f32 / self.logic_ratio) as u32; let cycle_limit =
(self.logic_frequency as f32 / self.visual_frequency).round() as u32;
loop { loop {
// limits the number of ticks to the typical number // limits the number of ticks to the typical number
// of cycles expected for the current logic cycle // of cycles expected for the current logic cycle
if counter_cycles >= cycle_limit { if counter_cycles >= cycle_limit {
pending_cycles = counter_cycles - cycle_limit;
break; break;
} }
...@@ -266,12 +275,22 @@ impl Emulator { ...@@ -266,12 +275,22 @@ impl Emulator {
} }
} }
let logic_frequency = // calculates the number of ticks that have elapsed since the
GameBoy::CPU_FREQ as f32 / GameBoy::LCD_CYCLES as f32 * self.logic_ratio; // 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 // updates the next update time reference to the current
// time so that it can be used from game loop control // 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; self.next_tick_time_i = self.next_tick_time.ceil() as u32;
} }
......
...@@ -44,12 +44,7 @@ impl Graphics { ...@@ -44,12 +44,7 @@ impl Graphics {
// creates an accelerated canvas to be used in the drawing // creates an accelerated canvas to be used in the drawing
// then clears it so that is can be presented empty initially // then clears it so that is can be presented empty initially
let mut canvas = window let mut canvas = window.into_canvas().accelerated().build().unwrap();
.into_canvas()
.accelerated()
.present_vsync()
.build()
.unwrap();
canvas.set_logical_size(width, height).unwrap(); canvas.set_logical_size(width, height).unwrap();
canvas.clear(); canvas.clear();
......
...@@ -29,9 +29,24 @@ import info from "../package.json"; ...@@ -29,9 +29,24 @@ import info from "../package.json";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const require: any; declare const require: any;
/**
* The frequency at which the Game Boy emulator should
* run "normally".
*/
const LOGIC_HZ = 4194304; 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; 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 IDLE_HZ = 10;
const DISPLAY_WIDTH = 160; const DISPLAY_WIDTH = 160;
...@@ -215,16 +230,6 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ...@@ -215,16 +230,6 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
// reached the flush of the "tick" logic is skipped // reached the flush of the "tick" logic is skipped
if (currentTime < this.nextTickTime) return pending; 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 // initializes the counter of cycles with the pending number
// of cycles coming from the previous tick // of cycles coming from the previous tick
let counterCycles = pending; let counterCycles = pending;
...@@ -287,8 +292,21 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ...@@ -287,8 +292,21 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
this.frameStart = currentTime; this.frameStart = currentTime;
} }
// updates the next update time reference to the, so that it // calculates the number of ticks that have elapsed since the
// can be used to control the game loop // 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; this.nextTickTime += (1000 / this.visualFrequency) * ticks;
// calculates the new number of pending (overflow) cycles // calculates the new number of pending (overflow) cycles
......
...@@ -219,10 +219,14 @@ impl GameBoy { ...@@ -219,10 +219,14 @@ impl GameBoy {
/// Gameboy implementations that are meant with performance /// Gameboy implementations that are meant with performance
/// in mind and that do not support WASM interface of copy. /// in mind and that do not support WASM interface of copy.
impl GameBoy { impl GameBoy {
/// The logical frequency of the Game Boy /// The logic frequency of the Game Boy
/// CPU in hz. /// CPU in hz.
pub const CPU_FREQ: u32 = 4194304; 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 /// The cycles taken to run a complete frame
/// loop in the Game Boy's PPU (in CPU cycles). /// loop in the Game Boy's PPU (in CPU cycles).
pub const LCD_CYCLES: u32 = 70224; pub const LCD_CYCLES: u32 = 70224;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment