Major performance improvements

This commit is contained in:
2026-07-05 22:29:09 -05:00
parent d13f30d135
commit 04e6080780
12 changed files with 251 additions and 43 deletions

47
src/bin/bench/main.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::{path::PathBuf, time::Instant};
use nes_emu::{Break, NES};
use tracing_subscriber::EnvFilter;
use clap::Parser;
extern crate nes_emu;
#[derive(Parser)]
#[command(version, about)]
struct Args {
file: PathBuf,
#[arg(short, long)]
frames: Option<usize>,
}
fn main() -> Result<(), anyhow::Error> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let args = Args::parse();
if let Some(frames) = args.frames {
println!("Running {frames} frames of {} as fast as possible", args.file.display());
} else {
println!("Running {} as fast as possible", args.file.display());
}
let mut n = NES::load_nes_file(args.file).unwrap();
n.reset();
let start = Instant::now();
let mut cur = 0;
loop {
while !n.halted() && !n.run_one_clock_cycle(&Break::default()).ppu_frame {
}
n.apu_mut().reset_frame_samples();
cur += 1;
if args.frames.is_some_and(|f| f < cur) {
let time = start.elapsed();
println!("Completed {} in {}s", cur, time.as_secs_f64());
println!("~= {} fps", cur as f64 / time.as_secs_f64());
break Ok(());
}
// TODO: update currently held button in controller
}
// Ok(())
}

View File

@@ -25,6 +25,7 @@ pub struct Audio {
last: usize,
max: usize,
paused: Arc<AtomicBool>,
kill: Arc<AtomicBool>,
}
impl Audio {
@@ -39,12 +40,14 @@ impl Audio {
let (prod, mut cons) = SharedRb::new(BUFFER_SIZE * 1024 * 1024).split();
let paused = Arc::new(AtomicBool::new(true));
let paused_inner = Arc::clone(&paused);
let kill = Arc::new(AtomicBool::new(false));
let kill_inner = Arc::clone(&kill);
let stream = device
.build_output_stream(
&cpal::StreamConfig {
channels: 1,
sample_rate: 60 * 3723,
sample_rate: 60 * 3722,
buffer_size: cpal::BufferSize::Fixed(BUFFER_SIZE as FrameCount),
},
move |a: &mut [u8], _b| {
@@ -53,7 +56,12 @@ impl Audio {
a[taken..].fill(128);
}
},
|e| eprintln!("Audio: {e}"),
move |e| {
eprintln!("Audio: {e}");
if e == cpal::StreamError::BufferUnderrun {
kill_inner.store(true, std::sync::atomic::Ordering::Release);
}
},
None,
)
.unwrap();
@@ -64,6 +72,7 @@ impl Audio {
_stream: stream,
rb: prod,
paused,
kill,
last: 0,
max: 0,
}
@@ -74,6 +83,14 @@ impl Audio {
let _ = self._stream.pause();
}
pub fn killed(&self) -> bool {
self.kill.load(std::sync::atomic::Ordering::Acquire)
}
pub fn occupied(&self) -> usize {
self.rb.occupied_len()
}
pub fn submit(&mut self, samples: &[u8]) {
let start = self.rb.occupied_len();
self.max = self.max.max(self.last - start);

View File

@@ -233,7 +233,6 @@ impl DebuggerState {
column(
nes.debug_log()
.history()
.into_iter()
.rev()
.map(|s| text(s).line_height(0.9).into())
)

View File

@@ -5,17 +5,9 @@ use std::{
};
use iced::{
Element,
Length::{Fill, Shrink},
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
keyboard::{self, Key, Modifiers, key::Named},
mouse, time,
widget::{
self, Canvas, button,
canvas::{Frame, Program},
column, container, image, row,
},
window::{self, Id, Settings},
exit, keyboard::{self, key::Named, Key, Modifiers}, mouse, time, widget::{
self, button, canvas::{Frame, Program}, column, container, image, row, Canvas
}, window::{self, Id, Settings}, Element, Length::{Fill, Shrink}, Point, Rectangle, Renderer, Size, Subscription, Task, Theme
};
use nes_emu::{
Break, NES,
@@ -313,6 +305,8 @@ impl Emulator {
} => {
if val == "t" {
self.nes.reset();
} else if val == "q" {
return exit();
}
}
keyboard::Event::KeyPressed {
@@ -377,6 +371,10 @@ impl Emulator {
},
Message::Periodic(_i) => {
if self.running {
// Better idea: audio-controlled timing. Increase timing frequency, but we only run long enough
// to keep the audio buffer full.
// We'd likely need some kind of double-buffering for ppu output
// TODO: Smarter frame skip
if self.prev[0].elapsed() >= Duration::from_millis(2) {
self.nes.run_one_clock_cycle(&Break::default());
@@ -391,7 +389,8 @@ impl Emulator {
}
}
self.prev[0] = Instant::now();
self.audio.submit(self.nes.apu().get_frame_samples());
let samples = self.nes.apu().get_frame_samples();
self.audio.submit(&samples[..samples.len().min(3722)]);
self.nes.apu_mut().reset_frame_samples();
}
} else {

View File

@@ -263,7 +263,9 @@ impl Cpu {
} else {
// debug!("Running 0x{:04X} {} :{:X} {:X?}", self.pc - (1 + params.len() as u16), $val, ins, params);
// debug!("Running 0x{:04X} {} :{:X} {:X?}", self.pc - (1 + params.len() as u16), $val, ins, params);
self.last_instruction = format!("0x{:04X} {} :{:X} {:X?}", addr, $val, ins, params);
if self.debug_log.enabled() {
self.last_instruction = format!("0x{:04X} {} :{:X} {:X?}", addr, $val, ins, params);
}
$(
let $name = params[0];
#[allow(unused_assignments)]

View File

@@ -1,10 +1,13 @@
// use std::num::NonZeroUsize;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct DebugLog {
enabled: bool,
current: String,
history: Vec<String>,
history: VecDeque<String>,
size: usize,
// max_history: Option<NonZeroUsize>,
// pos: usize,
}
@@ -14,7 +17,8 @@ impl DebugLog {
Self {
enabled: false,
current: String::new(),
history: vec![],
history: VecDeque::new(),
size: 0,
// max_history: None,
// pos: 0,
}
@@ -24,30 +28,24 @@ impl DebugLog {
// if self.current.len() > 500 {
let mut rot = std::mem::take(&mut self.current);
self.current = rot.split_off(rot.rfind('\n').unwrap_or(rot.len()));
// if let Some(max) = self.max_history {
// if self.history.len() < max.into() {
// self.history.extend(rot.lines().map(|s| s.to_owned()));
// } else {
// self.history[self.pos] = rot;
// self.pos = (self.pos + 1) % max.get();
// }
// } else {
// self.history.push(rot);
self.size += rot.len();
self.history.extend(rot.lines().map(|s| s.to_owned()));
// }
// }
while self.history.len() > 200 {
self.size -= self.history.pop_front().map_or(0, |s| s.len());
}
}
// pub fn current(&self) -> &str {
// &self.current
// }
pub fn history(&self) -> &[String] {
&self.history[self.history.len().saturating_sub(100)..]
pub fn history(&self) -> impl Iterator<Item = &str> + DoubleEndedIterator {
self.history.iter().map(|i| i.as_str())
// &self.history[self.history.len().saturating_sub(100)..]
}
pub fn pop(&mut self) -> Option<String> {
self.history.pop()
self.history.pop_back()
}
pub fn enable(&mut self) {

View File

@@ -345,7 +345,7 @@ impl NES {
}
pub fn image(&self) -> &RenderBuffer<256, 240> {
&self.ppu.render_buffer
&self.ppu.render_buffer_b
}
pub fn cpu(&self) -> &Cpu {

View File

@@ -242,7 +242,8 @@ impl OAM {
} else {
0x0000
} + 16 * (self.sprite_output_units[run / 8].tile & !1) as u16
+ off as u16 + if off > 7 { 8 } else { 0 })
+ off as u16
+ if off > 7 { 8 } else { 0 })
} else {
let off = if self.sprite_output_units[run / 8].attrs.vflip() {
7 - off
@@ -704,7 +705,10 @@ pub struct PPU {
pub palette: Palette,
pub background: Background,
pub oam: OAM,
pub render_buffer: RenderBuffer<256, 240>,
/// Current render buffer
pub render_buffer_a: RenderBuffer<256, 240>,
/// Output buffer (not rendering to)
pub render_buffer_b: RenderBuffer<256, 240>,
pub dbg_int: bool,
pub cycle: usize,
}
@@ -761,7 +765,8 @@ impl PPU {
even: false,
scanline: 0,
pixel: 25,
render_buffer: RenderBuffer::empty(),
render_buffer_a: RenderBuffer::empty(),
render_buffer_b: RenderBuffer::empty(),
background: Background {
v: 0,
t: 0,
@@ -953,9 +958,15 @@ impl PPU {
self.background.v = (self.background.v & 0b0000_0100_0001_1111)
| (self.background.t & 0b0111_1011_1110_0000);
}
if self.scanline != 261 {
self.oam.ppu_cycle(self.pixel, self.scanline, mem);
}
self.oam.ppu_cycle(
self.pixel,
if self.scanline == 261 {
0
} else {
self.scanline - 1
},
mem,
);
if self.pixel == 0 {
if self.scanline == 1 {
// let h_scroll_offset = self.background.x as usize + ((self.background.t as usize & 0b11) << 3);
@@ -1004,7 +1015,7 @@ impl PPU {
bg_color
};
// Write to screen
self.render_buffer.write(
self.render_buffer_a.write(
self.scanline,
self.pixel - 1,
self.palette.color(color),
@@ -1112,6 +1123,7 @@ impl PPU {
}
if self.scanline == 241 && self.pixel == 1 {
self.vblank = true;
std::mem::swap(&mut self.render_buffer_a, &mut self.render_buffer_b);
return true;
}
return false;