Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s
549 lines
20 KiB
Rust
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()]
|
|
}
|
|
}
|