nesfuzz/src/main.rs

274 lines
10 KiB
Rust
Raw Normal View History

2019-11-12 00:04:07 +00:00
mod cpu;
mod ppu;
2019-11-27 01:11:51 +00:00
mod apu;
2019-11-12 00:04:07 +00:00
mod cartridge;
mod input;
2019-11-27 01:11:51 +00:00
mod screen;
2019-12-20 00:13:48 +00:00
mod audio;
mod state;
2019-11-12 00:04:07 +00:00
use cpu::Cpu;
use ppu::Ppu;
2019-11-27 01:11:51 +00:00
use apu::Apu;
2020-03-01 23:47:43 +00:00
use cartridge::{check_signature, get_mapper};
2019-11-12 00:04:07 +00:00
use input::poll_buttons;
2019-11-27 01:11:51 +00:00
use screen::{init_window, draw_pixel, draw_to_window};
use state::{save_state, load_state, find_next_filename, find_last_save_state};
2019-11-12 00:04:07 +00:00
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Instant, Duration};
2020-03-01 20:17:09 +00:00
use sdl2::Sdl;
use sdl2::render::{Canvas, Texture};
2019-11-12 00:04:07 +00:00
use sdl2::keyboard::Keycode;
2020-03-01 07:22:52 +00:00
use sdl2::EventPump;
2019-11-12 00:04:07 +00:00
use sdl2::event::Event;
use sdl2::pixels::PixelFormatEnum;
2020-03-01 20:17:09 +00:00
use sdl2::video::Window;
use sdl2::messagebox::*;
2019-11-12 00:04:07 +00:00
// use cpuprofiler::PROFILER;
2020-03-01 22:12:51 +00:00
enum GameExitMode {
QuitApplication,
NewGame(String),
2020-03-02 00:28:45 +00:00
Reset,
Nothing,
2020-03-01 22:12:51 +00:00
}
2019-11-12 00:04:07 +00:00
fn main() -> Result<(), String> {
// Set up screen
2019-11-27 01:11:51 +00:00
let sdl_context = sdl2::init()?;
let mut event_pump = sdl_context.event_pump()?;
let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window");
let mut texture = texture_creator.create_texture_streaming(
PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32)
2019-11-27 01:11:51 +00:00
.map_err(|e| e.to_string())?;
let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor
let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed
let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen
2020-03-01 20:17:09 +00:00
let argv = std::env::args().collect::<Vec<String>>();
let mut filename = if argv.len() > 1 {
2020-03-01 20:17:09 +00:00
argv[1].to_string()
} else {
2020-03-01 22:11:24 +00:00
show_simple_message_box(
MessageBoxFlag::INFORMATION, "Welcome to Nestur!", INSTRUCTIONS, canvas.window()
).map_err(|e| e.to_string())?;
2020-03-01 20:17:09 +00:00
let name;
'waiting: loop {
for event in event_pump.poll_iter() {
match event {
2020-03-01 22:11:24 +00:00
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. }
=> return Ok(()),
2020-03-01 20:17:09 +00:00
Event::DropFile{ filename: f, .. } => {
match check_signature(&f) {
Ok(()) => {
name = f;
break 'waiting;
},
Err(e) => println!("{}", e),
}
2020-03-01 20:17:09 +00:00
},
2020-03-08 23:01:24 +00:00
_ => (),
2020-03-01 20:17:09 +00:00
}
}
2020-03-01 22:11:24 +00:00
std::thread::sleep(Duration::from_millis(100));
2020-03-01 20:17:09 +00:00
}
name
};
loop {
let res = run_game(&sdl_context, &mut event_pump, &mut screen_buffer, &mut canvas, &mut texture, &filename);
match res {
2020-03-02 00:28:45 +00:00
Ok(Some(GameExitMode::Reset)) => (),
Ok(Some(GameExitMode::NewGame(next_file))) => filename = next_file,
Ok(None) | Ok(Some(GameExitMode::QuitApplication)) => return Ok(()),
Err(e) => return Err(e),
2020-03-02 00:28:45 +00:00
Ok(Some(GameExitMode::Nothing)) => panic!("shouldn't have returned exit mode Nothing to main()"),
}
}
2020-03-01 20:17:09 +00:00
}
fn run_game(
sdl_context: &Sdl,
event_pump: &mut EventPump,
screen_buffer: &mut Vec<u8>,
canvas: &mut Canvas<Window>,
texture: &mut Texture,
filename: &str
) -> Result<Option<GameExitMode>, String> {
println!("loading game {}", filename);
2020-03-01 20:17:09 +00:00
2019-12-20 00:13:48 +00:00
// Set up audio
let mut temp_buffer = vec![]; // receives one sample each time the APU ticks. this is a staging buffer so we don't have to lock the mutex too much.
let apu_buffer = Arc::new(Mutex::new(Vec::<f32>::new())); // stays in this thread, receives raw samples between frames
let sdl_buffer = Arc::clone(&apu_buffer); // used in audio device's callback to select the samples it needs
2020-03-01 20:17:09 +00:00
let audio_device = audio::initialize(sdl_context, sdl_buffer).expect("Could not create audio device");
2019-12-20 00:13:48 +00:00
let mut half_cycle = false;
let mut audio_started = false;
2019-12-20 00:13:48 +00:00
// Initialize hardware components
2020-03-01 20:17:09 +00:00
let filepath = Path::new(filename).to_path_buf();
let mapper = get_mapper(filename.to_string());
2020-01-10 04:04:10 +00:00
let ppu = Ppu::new(mapper.clone());
2019-11-27 01:11:51 +00:00
let apu = Apu::new();
2020-01-10 04:04:10 +00:00
let mut cpu = Cpu::new(mapper.clone(), ppu, apu);
2019-11-12 00:04:07 +00:00
// For throttling to 60 FPS
2019-11-12 00:04:07 +00:00
let mut timer = Instant::now();
let mut fps_timer = Instant::now();
let mut fps = 0;
// PROFILER.lock().unwrap().start("./main.profile").unwrap();
'running: loop {
2019-12-20 00:13:48 +00:00
// step CPU: perform 1 cpu instruction, getting back number of clock cycles it took
let cpu_cycles = cpu.step();
// clock APU every other CPU cycle
let mut apu_cycles = cpu_cycles / 2;
if cpu_cycles & 1 == 1 { // if cpu step took an odd number of cycles
if half_cycle { // and we have a half-cycle stored
apu_cycles += 1; // use it
half_cycle = false;
} else {
half_cycle = true; // or save it for next odd cpu step
}
}
for _ in 0..apu_cycles {
2020-08-08 16:25:31 +00:00
// can't read CPU from APU so have to pass byte in here
let sample_byte = cpu.read(cpu.apu.dmc.current_address);
temp_buffer.push(cpu.apu.clock(sample_byte));
2019-12-20 00:13:48 +00:00
}
// clock PPU three times for every CPU cycle
for _ in 0..cpu_cycles * 3 {
let (pixel, end_of_frame) = cpu.ppu.clock();
2019-11-12 00:04:07 +00:00
match pixel {
2020-03-01 20:17:09 +00:00
Some((x, y, color)) => draw_pixel(screen_buffer, x, y, color),
None => (),
};
2019-11-12 00:04:07 +00:00
if end_of_frame {
2019-11-27 01:11:51 +00:00
fps += 1; // keep track of how many frames we've rendered this second
2020-03-01 20:17:09 +00:00
draw_to_window(texture, canvas, &screen_buffer)?; // draw the buffer to the window with SDL
2020-01-14 06:12:24 +00:00
let mut b = apu_buffer.lock().unwrap(); // unlock mutex to the real buffer
b.append(&mut temp_buffer); // send this frame's audio data, emptying the temp buffer
if !audio_started {
audio_started = true;
audio_device.resume();
}
2019-11-12 00:04:07 +00:00
let now = Instant::now();
2019-11-27 01:11:51 +00:00
// if we're running faster than 60Hz, kill time
2019-11-12 00:04:07 +00:00
if now < timer + Duration::from_millis(1000/60) {
std::thread::sleep(timer + Duration::from_millis(1000/60) - now);
}
timer = Instant::now();
let outcome = process_events(event_pump, &filepath, &mut cpu);
match outcome {
GameExitMode::QuitApplication => break 'running,
2020-03-02 00:28:45 +00:00
GameExitMode::Reset => return Ok(Some(GameExitMode::Reset)),
GameExitMode::NewGame(g) => return Ok(Some(GameExitMode::NewGame(g))),
2020-03-02 00:28:45 +00:00
GameExitMode::Nothing => (),
2019-11-12 00:04:07 +00:00
}
}
}
// handle keyboard events
match poll_buttons(&cpu.strobe, &event_pump) {
Some(button_states) => cpu.button_states = button_states,
None => (),
};
// calculate fps
let now = Instant::now();
if now > fps_timer + Duration::from_secs(1) {
println!("frames per second: {}", fps);
2019-11-12 00:04:07 +00:00
fps = 0;
fps_timer = now;
}
}
// PROFILER.lock().unwrap().stop().unwrap();
mapper.borrow().save_battery_backed_ram();
Ok(None)
2019-11-12 00:04:07 +00:00
}
fn process_events(event_pump: &mut EventPump, filepath: &PathBuf, cpu: &mut Cpu) -> GameExitMode {
2020-03-01 07:22:52 +00:00
for event in event_pump.poll_iter() {
match event {
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. }
=> return GameExitMode::QuitApplication,
2020-03-02 00:28:45 +00:00
Event::KeyDown{ keycode: Some(Keycode::F2), .. }
=> return GameExitMode::Reset,
2020-03-01 07:22:52 +00:00
Event::KeyDown{ keycode: Some(Keycode::F5), .. } => {
let save_file = find_next_filename(filepath, Some("dat"))
.expect("could not generate save state filename");
let res: Result<(), String> = save_state(cpu, &save_file)
2020-03-01 07:22:52 +00:00
.or_else(|e| {println!("{}", e); Ok(())});
res.unwrap();
},
Event::KeyDown{ keycode: Some(Keycode::F9), .. } => {
match find_last_save_state(filepath, Some("dat")) {
Some(p) => {
let res: Result<(), String> = load_state(cpu, &p)
.or_else(|e| { println!("{}", e); Ok(()) } );
res.unwrap();
},
None => println!("no save state found for {:?}", filepath)
}
2020-03-01 07:22:52 +00:00
},
Event::DropFile{ timestamp: _t, window_id: _w, filename: f } => {
2020-03-01 22:12:51 +00:00
if f.len() > 4 && &f[f.len()-4..] == ".dat" {
let p = Path::new(&f).to_path_buf();
let res: Result<(), String> = load_state(cpu, &p)
.or_else(|e| {println!("{}", e); Ok(())});
res.unwrap();
2020-03-01 23:47:43 +00:00
// } else if f.len() > 4 && &f[f.len()-4..] == ".nes" {
} else {
match check_signature(&f) {
2020-03-01 23:47:43 +00:00
Ok(()) => return GameExitMode::NewGame(f),
Err(e) => println!("{}", e),
}
2020-03-01 22:12:51 +00:00
}
2020-03-01 07:22:52 +00:00
},
_ => (),
}
}
2020-03-02 00:28:45 +00:00
return GameExitMode::Nothing
2020-03-01 07:22:52 +00:00
}
2020-03-01 22:11:24 +00:00
const INSTRUCTIONS: &str = "To play a game, drag an INES file (extension .nes) onto the main window.
To save the game state, press F5. To load the most recent save state, press F9.
To load another save state file, drag a .dat file onto the window while the game is running.
2020-03-02 00:28:45 +00:00
Battery-backed RAM saves (what the NES cartridges have) will be written to a .sav file if used.
2020-03-02 00:44:32 +00:00
To reset the console/current game, press F2.
Controls
------------
A: D
B: F
Start: enter
Select: (right) shift
Up/Down/Left/Right: arrow keys
";
2020-03-01 22:11:24 +00:00
/*
TODO:
- untangle CPU and APU/PPU?
2020-03-05 03:54:52 +00:00
- better save file organization?
2019-12-31 23:22:44 +00:00
Timing notes:
The PPU is throttled to 60Hz by sleeping in the main loop. This locks the CPU to roughly its intended speed, 1.789773MHz NTSC. The APU runs at half that.
The APU gives all of its samples to the SDL audio device, which takes them 60 times per second in batches of 735 (44,100/60). It selects the ones
it needs at the proper interval and truncates its buffer.
2020-01-04 05:48:07 +00:00
Failed tests from instr_test-v5/rom_singles/:
3, immediate, Failed. Just unofficial instructions?
0B AAC #n
2B AAC #n
4B ASR #n
6B ARR #n
AB ATX #n
CB AXS #n
7, abs_xy, 'illegal opcode using abs x: 9c'
*/