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::() .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, 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) { 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 { let (win, task) = iced::window::open(Settings::default()); self.windows.insert(win, ty); return task.discard(); } fn update(&mut self, message: Message) -> Task { 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 { 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 for Emulator { type State = (); fn update( &self, _state: &mut Self::State, _event: &iced::Event, _bounds: Rectangle, _cursor: iced::advanced::mouse::Cursor, ) -> Option> { // ~ 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> { // 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()] } }