use std::{collections::HashMap, fmt, time::Duration}; use iced::{ Color, Element, Font, Length::{Fill, Shrink}, Point, Renderer, Size, Subscription, Task, Theme, mouse, widget::{ Canvas, button, canvas::{Frame, Program}, column, container, row, text, }, window::{self, Id, Settings}, }; use nes_emu::{ NES, PPU, debugger::{DebuggerMessage, DebuggerState}, header_menu::header_menu, hex_view::{HexEvent, HexView}, resize_watcher::resize_watcher, }; use tokio::runtime::Runtime; 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 = "./Super Mario Bros. (World).nes"; 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, PartialEq, Eq)] enum MemoryTy { Cpu, } #[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), 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::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"), } } } struct Emulator { nes: NES, windows: HashMap, debugger: DebuggerState, main_win_size: Size, } #[derive(Debug, Clone)] enum Message { Tick(usize), Frame, DMA, CPU, DebugInt, WindowClosed(Id), Header(HeaderButton), Hex(Id, HexEvent), Debugger(DebuggerMessage), SetSize(window::Id, Size), } 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(); let (win, task) = iced::window::open(Settings { min_size: None, ..Settings::default() }); ( Self { nes, windows: HashMap::from_iter([(win, WindowType::Main)]), debugger: DebuggerState::new(), main_win_size: Size::new(0., 0.), }, task.discard(), ) } fn title(&self, win: Id) -> String { match self.windows.get(&win) { Some(WindowType::Main) => "NES emu".into(), Some(WindowType::Memory(_, _)) => "NES MemoryView".into(), 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::Header(HeaderButton::Open(w)) => { return self.open(w); } 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)); } } // self.image.0.clone_from(self.nes.image()); Task::none() } fn subscriptions(&self) -> Subscription { Subscription::batch([ window::close_events().map(Message::WindowClosed), // window::events().map(Message::Window), // window::resize_events().map(Message::WindowResized), ]) } fn view(&self, win: Id) -> Element<'_, Message> { 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(self.nes.cpu_mem()) .map(move |e| Message::Hex(win, e)), }; let content = column![hex].width(Fill).height(Fill); container(content).width(Fill).height(Fill).into() } Some(WindowType::TileMap) => { container(Canvas::new(DbgImage::NameTable(self.nes.ppu()))) .width(Fill) .height(Fill) .into() } Some(WindowType::TileViewer) => { container(Canvas::new(DbgImage::PatternTable(self.nes.ppu()))) .width(Fill) .height(Fill) .into() } Some(WindowType::Palette) => container(Canvas::new(DbgImage::Palette(self.nes.ppu()))) .width(Fill) .height(Fill) .into(), Some(WindowType::Debugger) => { container(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::Reset, HeaderButton::PowerCycle,], Message::Header ), header_menu( "Debugging", [ HeaderButton::Open(WindowType::Debugger), HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, 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 draw( &self, _state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: iced::Rectangle, _cursor: mouse::Cursor, ) -> Vec> { // const SIZE: f32 = 2.; let mut frame = Frame::new( renderer, bounds.size(), // iced::Size { // width: 256. * 2., // height: 240. * 2., // }, ); frame.scale(2.); 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), ); } } vec![frame.into_geometry()] } } enum DbgImage<'a> { NameTable(&'a PPU), PatternTable(&'a PPU), Palette(&'a PPU), } impl Program for DbgImage<'_> { type State = (); fn draw( &self, _state: &Self::State, renderer: &Renderer, _theme: &Theme, _bounds: iced::Rectangle, _cursor: mouse::Cursor, ) -> Vec> { // const SIZE: f32 = 2.; let mut name_table_frame = Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.)); name_table_frame.scale(2.); match self { DbgImage::NameTable(ppu) => ppu.render_name_table(&mut name_table_frame), DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame), DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame), } vec![name_table_frame.into_geometry()] } }