Files
nes-emu/src/main.rs
Matthew Pomes 3372559c19
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s
Working Sprite implementation
2026-02-10 22:17:51 -06:00

549 lines
20 KiB
Rust

use std::{
collections::HashMap,
fmt,
time::{Duration, Instant},
};
use iced::{
Color, Element,
Length::{Fill, Shrink},
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
keyboard::{self, Key, Modifiers, key::Named},
mouse, time,
widget::{
self, Button, Canvas, button,
canvas::{Frame, Program},
column, container, image, row,
},
window::{self, Id, Settings},
};
use nes_emu::{
Break, NES,
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
header_menu::header_menu,
hex_view::{HexEvent, HexView},
resize_watcher::resize_watcher,
};
use tokio::{io::AsyncWriteExt, runtime::Runtime};
use tracing::instrument::WithSubscriber;
use tracing_subscriber::EnvFilter;
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_red.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "render-updating.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "input_test.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling.nes");
const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes");
// const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = "./cpu_timing_test.nes";
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
/// Disable button in debug mode - used to disable play/pause buttons since the performance isn't fast enough
fn debug_disable<'a, Message, Theme, Renderer>(
btn: Button<'a, Message, Theme, Renderer>,
) -> Button<'a, Message, Theme, Renderer>
where
Theme: button::Catalog + 'a,
Renderer: iced::advanced::Renderer,
{
if cfg!(debug_assertions) {
btn.on_press_maybe(None)
} else {
btn
}
}
extern crate nes_emu;
fn main() -> Result<(), iced::Error> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
iced::daemon(Emulator::new, Emulator::update, Emulator::view)
.subscription(Emulator::subscriptions)
.theme(Theme::Dark)
.title(Emulator::title)
.executor::<Runtime>()
.run()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MemoryTy {
Cpu,
PPU,
OAM,
}
impl fmt::Display for MemoryTy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Export {self:?} Memory")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum WindowType {
Main,
Memory(MemoryTy, HexView),
TileMap,
TileViewer,
Palette,
Debugger,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HeaderButton {
// OpenMemory,
// OpenTileMap,
// OpenTileViewer,
// OpenDebugger,
Open(WindowType),
OpenRom,
Reset,
PowerCycle,
}
impl fmt::Display for HeaderButton {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open(WindowType::Memory(MemoryTy::Cpu, _)) => write!(f, "Open Memory Viewer"),
Self::Open(WindowType::Memory(MemoryTy::PPU, _)) => write!(f, "Open PPU Memory Viewer"),
Self::Open(WindowType::Memory(MemoryTy::OAM, _)) => write!(f, "Open OAM Memory Viewer"),
Self::Open(WindowType::TileMap) => write!(f, "Open TileMap Viewer"),
Self::Open(WindowType::TileViewer) => write!(f, "Open Tile Viewer"),
Self::Open(WindowType::Debugger) => write!(f, "Open Debugger"),
Self::Open(WindowType::Palette) => write!(f, "Open Palette"),
Self::Open(WindowType::Main) => write!(f, "Create new Main window"),
Self::Reset => write!(f, "Reset"),
Self::PowerCycle => write!(f, "Power Cycle"),
Self::OpenRom => write!(f, "Open ROM file"),
}
}
}
struct Emulator {
running: bool,
nes: NES,
windows: HashMap<Id, WindowType>,
debugger: DebuggerState,
main_win_size: Size,
prev: [Instant; 2],
}
#[derive(Debug, Clone)]
enum Message {
OpenRom(NES),
// Tick(usize),
// Frame,
// DMA,
// CPU,
// DebugInt,
WindowClosed(Id),
WindowOpened(Id),
Key(keyboard::Event),
Periodic(Instant),
SetRunning(bool),
Header(HeaderButton),
Hex(Id, HexEvent),
Debugger(DebuggerMessage),
SetSize(window::Id, Size),
Export(MemoryTy),
}
impl Emulator {
fn new() -> (Self, Task<Message>) {
let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file");
nes.reset();
// TODO: remove
// let mut count = 10;
// while !nes.halted() {
// if nes.run_one_clock_cycle().ppu_frame {
// count -= 1;
// if count <= 0 {
// break;
// }
// }
// }
let (win, task) = iced::window::open(Settings {
min_size: None,
..Settings::default()
});
let (win_2, task_2) = iced::window::open(Settings {
min_size: None,
..Settings::default()
});
(
Self {
nes,
windows: HashMap::from_iter([
(win, WindowType::Main),
(win_2, WindowType::Debugger)
]),
debugger: DebuggerState::new(),
main_win_size: Size::new(0., 0.),
running: false,
prev: [Instant::now(); 2],
},
Task::batch([task, task_2]).discard()
// task.discard(),
)
}
fn title(&self, win: Id) -> String {
match self.windows.get(&win) {
Some(WindowType::Main) => "NES emu".into(),
Some(WindowType::Memory(ty, _)) => format!("NES {ty:?} Memory"),
Some(WindowType::TileMap) => "NES TileMap".into(),
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
Some(WindowType::Palette) => "NES Palette Viewer".into(),
Some(WindowType::Debugger) => "NES Debugger".into(),
None => todo!(),
}
}
fn open(&mut self, ty: WindowType) -> Task<Message> {
let (win, task) = iced::window::open(Settings::default());
self.windows.insert(win, ty);
return task.discard();
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
// Message::Tick(count) => {
// for _ in 0..count {
// self.nes.run_one_clock_cycle();
// }
// }
// Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
// Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
// Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
// Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
Message::WindowClosed(id) => {
if let Some(WindowType::Main) = self.windows.remove(&id) {
return iced::exit();
}
}
Message::WindowOpened(_id) => {
// if let Some(WindowType::Main) = self.windows.get(&id) {
// // println!("Running resize");
// return iced::window::resize(id, self.main_win_size);
// }
}
Message::Header(HeaderButton::Open(w)) => {
return self.open(w);
}
Message::Header(HeaderButton::OpenRom) => {
return Task::future(
rfd::AsyncFileDialog::new()
.set_directory(".")
.add_filter("NES", &["nes"])
.set_title("Open NES Rom file")
.pick_file(),
)
.and_then(|p| {
Task::future(async move {
// println!("Opening: {}", p.path().display());
NES::async_load_nes_file(p.path()).await.ok()
})
})
.and_then(|n| Task::done(Message::OpenRom(n)));
}
Message::Hex(id, val) => {
if let Some(WindowType::Memory(_, view)) = self.windows.get_mut(&id) {
return view.update(val).map(move |e| Message::Hex(id, e));
}
}
Message::Header(HeaderButton::Reset) => {
self.nes.reset();
}
Message::Header(HeaderButton::PowerCycle) => {
self.nes.power_cycle();
}
Message::Debugger(debugger_message) => {
self.debugger.update(debugger_message, &mut self.nes)
}
Message::SetSize(id, size) => {
if let Some(WindowType::Main) = self.windows.get(&id) {
self.main_win_size = size;
}
return Task::future(async {
tokio::time::sleep(Duration::from_millis(50)).await;
})
.then(move |_| iced::window::resize(id, size));
}
Message::OpenRom(nes) => {
self.nes = nes;
self.nes.power_cycle();
}
Message::Export(ty) => {
let raw: Vec<_> = match ty {
MemoryTy::Cpu => (0..=0xFFFF)
.map(|i| self.nes.mem().peek_cpu(i).unwrap_or(0))
.collect(),
MemoryTy::PPU => (0..=0xFFFF)
.map(|i| self.nes.mem().peek_ppu(i).unwrap_or(0))
.collect(),
MemoryTy::OAM => (0..=0xFF).map(|i| self.nes.ppu().peek_oam(i)).collect(),
};
return Task::future(async move {
if let Some(file) = rfd::AsyncFileDialog::new()
.set_directory(".")
.set_file_name("output.dmp")
.set_title("Save memory dump")
.save_file()
.await
{
tokio::fs::File::create(file.path())
.await
.expect("Failed to save file")
.write_all(&raw)
.await
.expect("Failed to write dump");
}
})
.discard();
}
Message::Key(key) => match key {
keyboard::Event::KeyPressed {
key: Key::Character(val),
modifiers: Modifiers::CTRL,
repeat: false,
..
} => {
if val == "t" {
self.nes.reset();
}
}
keyboard::Event::KeyPressed {
key,
modifiers: Modifiers::NONE,
repeat: false,
..
} => {
if key == Key::Character("z".into()) {
self.nes.controller_1().set_a(true);
} else if key == Key::Character("x".into()) {
self.nes.controller_1().set_b(true);
} else if key == Key::Character("a".into()) {
self.nes.controller_1().set_select(true);
} else if key == Key::Character("s".into()) {
self.nes.controller_1().set_start(true);
} else if key == Key::Named(Named::ArrowDown) {
self.nes.controller_1().set_down(true);
} else if key == Key::Named(Named::ArrowUp) {
self.nes.controller_1().set_up(true);
} else if key == Key::Named(Named::ArrowLeft) {
self.nes.controller_1().set_left(true);
} else if key == Key::Named(Named::ArrowRight) {
self.nes.controller_1().set_right(true);
}
}
keyboard::Event::KeyReleased {
key,
modifiers: Modifiers::NONE,
..
} => {
if key == Key::Character("z".into()) {
self.nes.controller_1().set_a(false);
} else if key == Key::Character("x".into()) {
self.nes.controller_1().set_b(false);
} else if key == Key::Character("a".into()) {
self.nes.controller_1().set_select(false);
} else if key == Key::Character("s".into()) {
self.nes.controller_1().set_start(false);
} else if key == Key::Named(Named::ArrowDown) {
self.nes.controller_1().set_down(false);
} else if key == Key::Named(Named::ArrowUp) {
self.nes.controller_1().set_up(false);
} else if key == Key::Named(Named::ArrowLeft) {
self.nes.controller_1().set_left(false);
} else if key == Key::Named(Named::ArrowRight) {
self.nes.controller_1().set_right(false);
}
}
_ => (),
},
Message::Periodic(i) => {
if self.running {
// TODO: this should skip updating to avoid multiple frame skips
self.prev[1] = self.prev[0];
self.prev[0] = i;
self.nes.run_one_clock_cycle(&Break::default());
while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame {}
}
}
Message::SetRunning(running) => self.running = running,
}
// self.image.0.clone_from(self.nes.image());
Task::none()
}
fn subscriptions(&self) -> Subscription<Message> {
Subscription::batch([
window::close_events().map(Message::WindowClosed),
window::open_events().map(Message::WindowOpened),
keyboard::listen().map(Message::Key),
time::every(Duration::from_millis(1000 / 60)).map(Message::Periodic),
])
}
fn view(&self, win: Id) -> Element<'_, Message> {
// println!("Running view");
match self.windows.get(&win) {
Some(WindowType::Main) => {
let content = column![
self.dropdowns(),
Element::from(Canvas::new(self).width(256. * 2.).height(240. * 2.)),
// self.cpu_state(),
// self.controls(),
]
.height(Shrink);
resize_watcher(content)
.on_resize(move |s| Message::SetSize(win, s))
.width(Shrink)
.height(Shrink)
.into()
}
Some(WindowType::Memory(ty, view)) => {
let hex = match ty {
MemoryTy::Cpu => view
.render_cpu(self.nes.mem())
.map(move |e| Message::Hex(win, e)),
MemoryTy::PPU => view
.render_ppu(self.nes.mem(), self.nes.ppu())
.map(move |e| Message::Hex(win, e)),
MemoryTy::OAM => view
.render_oam(self.nes.ppu())
.map(move |e| Message::Hex(win, e)),
};
let content = column![row![header_menu("Export", [*ty], Message::Export)], hex]
.width(Fill)
.height(Fill);
container(content).width(Fill).height(Fill).into()
}
Some(WindowType::TileMap) => {
dbg_image(DbgImage::NameTable(self.nes.mem(), self.nes.ppu())).into()
}
Some(WindowType::TileViewer) => {
dbg_image(DbgImage::PatternTable(self.nes.mem(), self.nes.ppu())).into()
}
Some(WindowType::Palette) => {
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
}
Some(WindowType::Debugger) => column![
row![
debug_disable(
button(image("./images/ic_fluent_play_24_filled.png"))
.on_press(Message::SetRunning(true))
),
debug_disable(
button(image("./images/ic_fluent_pause_24_filled.png"))
.on_press(Message::SetRunning(false))
),
],
self.debugger.view(&self.nes).map(Message::Debugger)
]
.width(Fill)
.height(Fill)
.into(),
None => panic!("Window not found"),
// _ => todo!(),
}
}
fn dropdowns(&self) -> Element<'_, Message> {
row![
header_menu(
"Console",
[
HeaderButton::OpenRom,
HeaderButton::Reset,
HeaderButton::PowerCycle,
],
Message::Header
),
header_menu(
"Debugging",
[
HeaderButton::Open(WindowType::Debugger),
HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, HexView {})),
HeaderButton::Open(WindowType::Memory(MemoryTy::PPU, HexView {})),
HeaderButton::Open(WindowType::Memory(MemoryTy::OAM, HexView {})),
HeaderButton::Open(WindowType::TileMap),
HeaderButton::Open(WindowType::TileViewer),
HeaderButton::Open(WindowType::Palette),
],
Message::Header
)
]
.width(Fill)
.into()
}
}
impl Program<Message> for Emulator {
type State = ();
fn update(
&self,
_state: &mut Self::State,
_event: &iced::Event,
_bounds: Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Option<widget::Action<Message>> {
// ~ 60 fps, I think?
// Some(widget::Action::request_redraw_at(
// Instant::now() + Duration::from_millis(10),
// ))
// Some(widget::Action::request_redraw())
None
}
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// let start = Instant::now();
// const SIZE: f32 = 2.;
let mut frame = Frame::new(
renderer,
bounds.size(), // iced::Size {
// width: 256. * 2.,
// height: 240. * 2.,
// },
);
frame.scale(2.);
// TODO: use image for better? performance
frame.draw_image(
Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)),
widget::canvas::Image::new(widget::image::Handle::from_rgba(
256,
240,
self.nes.image().image(),
))
.filter_method(image::FilterMethod::Nearest)
.snap(true),
);
// for y in 0..240 {
// for x in 0..256 {
// let c = self.nes.image().read(y, x);
// frame.fill_rectangle(
// Point::new(x as f32, y as f32),
// Size::new(1., 1.),
// Color::from_rgb8(c.r, c.g, c.b),
// );
// }
// }
// println!("Rendered frame in {}ms", start.elapsed().as_millis());
vec![frame.into_geometry()]
}
}