Skip to content

Commit

Permalink
Add configurable, PAL/NTSC-aware framerate limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
twvd committed Dec 24, 2023
1 parent b30279b commit 2722041
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 39 deletions.
21 changes: 18 additions & 3 deletions src/bin/siena/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<()> {
Expand All @@ -118,21 +122,32 @@ 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
} else {
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;
Expand Down
18 changes: 0 additions & 18 deletions src/frontend/channel.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::sync::Arc;
use std::time::Instant;

use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, TrySendError};
Expand All @@ -13,9 +12,6 @@ pub struct ChannelRenderer {
receiver: Receiver<DisplayBuffer>,
width: usize,
height: usize,

fps_count: u64,
fps_time: Instant,
}

impl ChannelRenderer {
Expand All @@ -34,9 +30,6 @@ impl Renderer for ChannelRenderer {
receiver,
width,
height,

fps_count: 0,
fps_time: Instant::now(),
})
}

Expand All @@ -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),
Expand Down
14 changes: 1 addition & 13 deletions src/frontend/sdl.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,8 +37,6 @@ pub struct SDLRenderer {
#[allow(dead_code)]
height: usize,

last_frame: Instant,
frametime: u64,
fps_count: u64,
fps_time: Instant,
}
Expand Down Expand Up @@ -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(())
}
}
Expand Down Expand Up @@ -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(),
})
Expand Down
4 changes: 3 additions & 1 deletion src/snes/bus/mainbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ where
renderer: TRenderer,
joypads: [Joypad; JOYPAD_COUNT],
apu_verbose: bool,
fps: u64,
) -> Self {
Self {
cartridge,
Expand All @@ -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,
Expand Down Expand Up @@ -857,6 +858,7 @@ mod tests {
NullRenderer::new(0, 0).unwrap(),
joypads,
false,
0,
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/snes/cartridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion src/snes/ppu/ppu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,13 +40,25 @@ 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,

#[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,
Expand Down Expand Up @@ -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],

Expand All @@ -96,6 +112,9 @@ where
vmadd: Cell::new(0),
vmain: 0,
vram_prefetch: Cell::new(0),

last_frame: Instant::now(),
desired_frametime,
}
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/snes/ppu/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn ppustate() -> PPUState {
PPUState::new()
}
fn ppu() -> PPU<NullRenderer> {
PPU::new(NullRenderer {})
PPU::new(NullRenderer {}, 0)
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit 2722041

Please sign in to comment.