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;
|
2020-02-29 23:23:51 +00:00
|
|
|
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};
|
2020-03-06 01:50:56 +00:00
|
|
|
use state::{save_state, load_state, find_next_filename, find_last_save_state};
|
2019-11-12 00:04:07 +00:00
|
|
|
|
2020-03-01 18:36:34 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2020-01-14 05:36:54 +00:00
|
|
|
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;
|
2019-11-12 23:19:25 +00:00
|
|
|
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> {
|
2019-11-12 23:19:25 +00:00
|
|
|
// Set up screen
|
2019-11-27 01:11:51 +00:00
|
|
|
let sdl_context = sdl2::init()?;
|
|
|
|
let mut event_pump = sdl_context.event_pump()?;
|
2019-11-12 23:19:25 +00:00
|
|
|
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())?;
|
2019-11-12 23:19:25 +00:00
|
|
|
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>>();
|
2020-03-01 22:58:27 +00:00
|
|
|
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, .. } => {
|
2020-03-03 01:01:00 +00:00
|
|
|
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
|
|
|
|
};
|
2020-03-01 22:58:27 +00:00
|
|
|
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)) => (),
|
2020-03-01 22:58:27 +00:00
|
|
|
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 22:58:27 +00:00
|
|
|
}
|
|
|
|
}
|
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
|
2020-03-01 22:58:27 +00:00
|
|
|
) -> 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
|
2020-01-14 05:36:54 +00:00
|
|
|
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;
|
2020-03-06 01:50:56 +00:00
|
|
|
let mut audio_started = false;
|
2019-12-20 00:13:48 +00:00
|
|
|
|
2019-11-12 23:19:25 +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
|
|
|
|
2019-11-12 23:19:25 +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),
|
2019-11-12 23:19:25 +00:00
|
|
|
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
|
2020-03-06 01:50:56 +00:00
|
|
|
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();
|
2020-03-01 22:58:27 +00:00
|
|
|
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)),
|
2020-03-01 22:58:27 +00:00
|
|
|
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) {
|
2020-03-01 22:58:27 +00:00
|
|
|
println!("frames per second: {}", fps);
|
2019-11-12 00:04:07 +00:00
|
|
|
fps = 0;
|
|
|
|
fps_timer = now;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// PROFILER.lock().unwrap().stop().unwrap();
|
2020-01-15 02:51:59 +00:00
|
|
|
mapper.borrow().save_battery_backed_ram();
|
2020-03-01 22:58:27 +00:00
|
|
|
Ok(None)
|
2019-11-12 00:04:07 +00:00
|
|
|
}
|
|
|
|
|
2020-03-01 22:58:27 +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), .. }
|
2020-03-01 22:58:27 +00:00
|
|
|
=> 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), .. } => {
|
2020-03-01 18:36:34 +00:00
|
|
|
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), .. } => {
|
2020-03-06 01:50:56 +00:00
|
|
|
match find_last_save_state(filepath, Some("dat")) {
|
2020-03-01 18:36:34 +00:00
|
|
|
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 {
|
2020-03-03 01:01:00 +00:00
|
|
|
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
|
|
|
|
2019-12-31 02:10:27 +00:00
|
|
|
/*
|
2020-01-09 01:04:01 +00:00
|
|
|
|
2019-12-31 02:10:27 +00:00
|
|
|
TODO:
|
2020-01-15 02:51:59 +00:00
|
|
|
- untangle CPU and APU/PPU?
|
2020-03-05 03:54:52 +00:00
|
|
|
- better save file organization?
|
2019-12-31 02:10:27 +00:00
|
|
|
|
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.
|
2020-01-14 05:36:54 +00:00
|
|
|
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
|
|
|
|
2020-01-09 01:04:01 +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'
|
|
|
|
|
2019-12-31 02:10:27 +00:00
|
|
|
*/
|