Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s
335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
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::<Runtime>()
|
|
.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<Id, WindowType>,
|
|
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<Message>) {
|
|
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<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::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<Message> {
|
|
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<Message> for Emulator {
|
|
type State = ();
|
|
|
|
fn draw(
|
|
&self,
|
|
_state: &Self::State,
|
|
renderer: &Renderer,
|
|
_theme: &Theme,
|
|
bounds: iced::Rectangle,
|
|
_cursor: mouse::Cursor,
|
|
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
|
// 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<Message> for DbgImage<'_> {
|
|
type State = ();
|
|
|
|
fn draw(
|
|
&self,
|
|
_state: &Self::State,
|
|
renderer: &Renderer,
|
|
_theme: &Theme,
|
|
_bounds: iced::Rectangle,
|
|
_cursor: mouse::Cursor,
|
|
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
|
// 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()]
|
|
}
|
|
}
|