From 2722041c9299976ab5a4ffd40ebe7341bb830507 Mon Sep 17 00:00:00 2001 From: Thomas <thomas@thomasw.dev> Date: Sun, 24 Dec 2023 20:56:39 +0100 Subject: [PATCH] Add configurable, PAL/NTSC-aware framerate limiter --- src/bin/siena/main.rs | 21 ++++++++++++++++++--- src/frontend/channel.rs | 18 ------------------ src/frontend/sdl.rs | 14 +------------- src/snes/bus/mainbus.rs | 4 +++- src/snes/cartridge.rs | 2 +- src/snes/ppu/ppu.rs | 34 +++++++++++++++++++++++++++++++++- src/snes/ppu/tests.rs | 2 +- src/test/mod.rs | 2 +- 8 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/bin/siena/main.rs b/src/bin/siena/main.rs index 5b7261f..7d6d71d 100644 --- a/src/bin/siena/main.rs +++ b/src/bin/siena/main.rs @@ -16,7 +16,7 @@ use siena::frontend::sdl::{SDLEventPump, SDLRenderer}; use siena::frontend::Renderer; use siena::snes::bus::mainbus::{BusTrace, Mainbus}; use siena::snes::bus::Bus; -use siena::snes::cartridge::Cartridge; +use siena::snes::cartridge::{Cartridge, VideoFormat}; use siena::snes::cpu_65816::cpu::Cpu65816; use siena::snes::joypad::{Button, Joypad, JoypadEvent}; use siena::snes::ppu::ppu::{SCREEN_HEIGHT, SCREEN_WIDTH}; @@ -99,6 +99,10 @@ struct Args { /// Skip cartridge header detection, load as HiROM (mostly for test ROMs) #[arg(long)] no_header_hirom: bool, + + /// Override frame rate limit (0 = unlimited) + #[arg(long)] + fps: Option<u64>, } fn main() -> Result<()> { @@ -118,7 +122,7 @@ fn main() -> Result<()> { // Initialize cartridge let f = fs::read(args.filename)?; - let cart = if !args.no_header && !args.no_header_hirom { + let cartridge = if !args.no_header && !args.no_header_hirom { let c = Cartridge::load(&f); println!("Cartridge: {}", &c); c @@ -126,13 +130,24 @@ fn main() -> Result<()> { Cartridge::load_nohdr(&f, args.no_header_hirom) }; + // Determine frame rate limit, either based on the video format + // or what the user specified. + let fps = match args.fps { + None => match cartridge.get_video_format() { + VideoFormat::NTSC => 60, + VideoFormat::PAL => 50, + }, + Some(fps) => fps, + }; + // Initialize S-CPU bus let mut bus = Mainbus::<ChannelRenderer>::new( - cart, + cartridge, args.trace_bus, displaychannel, joypads, args.verbose, + fps, ); bus.apu.verbose = args.spc_verbose; bus.apu.ports.write().unwrap().trace = args.trace_apu_comm; diff --git a/src/frontend/channel.rs b/src/frontend/channel.rs index eee5257..46ca8e2 100644 --- a/src/frontend/channel.rs +++ b/src/frontend/channel.rs @@ -1,5 +1,4 @@ use std::sync::Arc; -use std::time::Instant; use anyhow::Result; use crossbeam_channel::{Receiver, Sender, TrySendError}; @@ -13,9 +12,6 @@ pub struct ChannelRenderer { receiver: Receiver<DisplayBuffer>, width: usize, height: usize, - - fps_count: u64, - fps_time: Instant, } impl ChannelRenderer { @@ -34,9 +30,6 @@ impl Renderer for ChannelRenderer { receiver, width, height, - - fps_count: 0, - fps_time: Instant::now(), }) } @@ -46,17 +39,6 @@ impl Renderer for ChannelRenderer { /// Renders changes to screen fn update(&mut self) -> Result<()> { - self.fps_count += 1; - - if self.fps_time.elapsed().as_secs() >= 2 { - println!( - "PPU Frame rate: {:0.2} frames/second", - self.fps_count as f32 / self.fps_time.elapsed().as_secs_f32() - ); - self.fps_count = 0; - self.fps_time = Instant::now(); - } - let buffer = std::mem::replace( &mut self.displaybuffer, new_displaybuffer(self.width, self.height), diff --git a/src/frontend/sdl.rs b/src/frontend/sdl.rs index ee1e2f8..0ccd525 100644 --- a/src/frontend/sdl.rs +++ b/src/frontend/sdl.rs @@ -1,8 +1,7 @@ use std::cell::RefCell; use std::sync::atomic::AtomicU8; use std::sync::Arc; -use std::thread::sleep; -use std::time::{Duration, Instant}; +use std::time::Instant; use anyhow::{anyhow, Result}; use sdl2::event::Event; @@ -38,8 +37,6 @@ pub struct SDLRenderer { #[allow(dead_code)] height: usize, - last_frame: Instant, - frametime: u64, fps_count: u64, fps_time: Instant, } @@ -70,13 +67,6 @@ impl SDLRenderer { self.fps_time = Instant::now(); } - // Limit the framerate - let framelen = self.last_frame.elapsed().as_micros() as u64; - if framelen < self.frametime { - //sleep(Duration::from_micros(self.frametime - framelen)); - } - self.last_frame = Instant::now(); - Ok(()) } } @@ -111,8 +101,6 @@ impl Renderer for SDLRenderer { displaybuffer: new_displaybuffer(width, height), width, height, - last_frame: Instant::now(), - frametime: 1000000 / 50, fps_count: 0, fps_time: Instant::now(), }) diff --git a/src/snes/bus/mainbus.rs b/src/snes/bus/mainbus.rs index 4840b32..b539dac 100644 --- a/src/snes/bus/mainbus.rs +++ b/src/snes/bus/mainbus.rs @@ -235,6 +235,7 @@ where renderer: TRenderer, joypads: [Joypad; JOYPAD_COUNT], apu_verbose: bool, + fps: u64, ) -> Self { Self { cartridge, @@ -244,7 +245,7 @@ where hdmaen: 0, joypads: Some(joypads), - ppu: PPU::<TRenderer>::new(renderer), + ppu: PPU::<TRenderer>::new(renderer, fps), apu: Apu::new(apu_verbose), memsel: 0, @@ -857,6 +858,7 @@ mod tests { NullRenderer::new(0, 0).unwrap(), joypads, false, + 0, ) } diff --git a/src/snes/cartridge.rs b/src/snes/cartridge.rs index 7bb884f..d95007c 100644 --- a/src/snes/cartridge.rs +++ b/src/snes/cartridge.rs @@ -93,7 +93,7 @@ impl Cartridge { (1 << self.rom[self.header_offset + HDR_RAMSIZE_OFFSET]) * 1024 } - fn get_video_format(&self) -> VideoFormat { + pub fn get_video_format(&self) -> VideoFormat { match self.rom[self.header_offset + HDR_DESTINATION_OFFSET] { 0 // Japan | 1 // North-America diff --git a/src/snes/ppu/ppu.rs b/src/snes/ppu/ppu.rs index a629811..4ac2f37 100644 --- a/src/snes/ppu/ppu.rs +++ b/src/snes/ppu/ppu.rs @@ -11,6 +11,8 @@ use crate::tickable::{Tickable, Ticks}; use std::cell::Cell; use std::sync::atomic::Ordering; use std::sync::Arc; +use std::thread::sleep; +use std::time::{Duration, Instant}; pub const SCREEN_WIDTH: usize = 8 * 32; pub const SCREEN_HEIGHT: usize = 8 * 28; @@ -38,6 +40,10 @@ fn _default_none<T>() -> Option<T> { None } +fn _default_now() -> Instant { + Instant::now() +} + #[derive(Serialize, Deserialize)] pub struct PPU<TRenderer: Renderer> { pub(super) vram: InnerVram, @@ -45,6 +51,14 @@ pub struct PPU<TRenderer: Renderer> { #[serde(skip, default = "_default_none")] pub renderer: Option<TRenderer>, + /// Timestamp when the last frame was completed + #[serde(skip, default = "_default_now")] + last_frame: Instant, + + /// The desired speed at which to run the PPU (and with that the emulator) + /// in microseconds per frame. + desired_frametime: u64, + pub(super) cycles: usize, pub(super) last_scanline: usize, pub(super) intreq_vblank: bool, @@ -75,7 +89,9 @@ where const VBLANK_START: usize = 0xE1; const LINE_HBLANK_START: usize = 274 * 4; - pub fn new(renderer: TRenderer) -> Self { + pub fn new(renderer: TRenderer, fps: u64) -> Self { + let desired_frametime = if fps == 0 { 0 } else { 1_000_000 / fps }; + Self { vram: vec![0; VRAM_WORDS], @@ -96,6 +112,9 @@ where vmadd: Cell::new(0), vmain: 0, vram_prefetch: Cell::new(0), + + last_frame: Instant::now(), + desired_frametime, } } @@ -217,16 +236,29 @@ where // Reload OAMADD self.state.oamadd_addr.set(self.state.oamadd_reload.get()); + // Roll over the VRAM buffer so changes can be made for the + // next frame. self.state.vram = Arc::new(self.vram.clone()); } } else { if self.vblank { + // VBlank period has ended self.vblank = false; // Send frame to the screen + // Wait for threadpool workers to finish all scanlines self.pool.join(); + + // Present frame to the screen let renderer = self.renderer.as_mut().unwrap(); renderer.update()?; + + // Sync to desired framerate + let frametime = self.last_frame.elapsed().as_micros() as u64; + if frametime < self.desired_frametime { + sleep(Duration::from_micros(self.desired_frametime - frametime)); + } + self.last_frame = Instant::now(); } // Scanline 0 is discarded by the original hardware, so diff --git a/src/snes/ppu/tests.rs b/src/snes/ppu/tests.rs index 1dad23e..6c62eff 100644 --- a/src/snes/ppu/tests.rs +++ b/src/snes/ppu/tests.rs @@ -8,7 +8,7 @@ fn ppustate() -> PPUState { PPUState::new() } fn ppu() -> PPU<NullRenderer> { - PPU::new(NullRenderer {}) + PPU::new(NullRenderer {}, 0) } #[test] diff --git a/src/test/mod.rs b/src/test/mod.rs index f558d5c..dfd868f 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -19,7 +19,7 @@ fn test_display(rom: &[u8], pass_hash: &[u8], time_limit: u128, stable: bool, hi let (display, dispstatus) = TestRenderer::new_test(SCREEN_WIDTH, SCREEN_HEIGHT); let (joypads, _) = Joypad::new_channel_all(); let cart = Cartridge::load_nohdr(rom, hirom); - let bus = Mainbus::<TestRenderer>::new(cart, BusTrace::None, display, joypads, false); + let bus = Mainbus::<TestRenderer>::new(cart, BusTrace::None, display, joypads, false, 0); let reset = bus.read16(0xFFFC); let mut cpu = Cpu65816::<Mainbus<TestRenderer>>::new(bus, reset);