Initial commit

This commit is contained in:
2025-12-07 11:34:37 -06:00
commit d97a8559ec
17 changed files with 8387 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
*.nes
*.zip

4560
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "nes-emu"
version = "0.1.0"
edition = "2024"
[dependencies]
bitfield = "0.19.3"
iced = { version = "0.13.1", features = ["debug", "canvas", "tokio", "lazy"] }
iced_graphics = { version = "0.13.0", features = ["geometry", "image"] }
iced_widget = { version = "0.13.4", features = ["canvas", "image"] }
thiserror = "2.0.17"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }

45
build.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::{io::Read, process::Stdio};
fn main() {
println!("cargo::rerun-if-changed=src/test_roms/");
println!("cargo::rerun-if-changed=build.rs");
let name = format!(
"{}/test-roms",
std::env::var("OUT_DIR").expect("Need OUT_DIR env var")
);
std::fs::create_dir_all(&name).expect("Failed to create rom output dir");
println!("cargo::rustc-env=ROM_DIR={name}");
for file in std::fs::read_dir("./src/test_roms").expect("Failed to read directory") {
let file = file.expect("test");
let file_name = file.file_name();
let file_name = file_name.to_str().unwrap();
if let Some(file_name) = file_name.strip_suffix(".asm") {
let mut proc = std::process::Command::new("/home/matthew/asm6f/asm6f")
.arg(file.file_name())
.arg(format!("{name}/{file_name}.nes"))
.current_dir("src/test_roms")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start process");
let rc = proc.wait();
if rc.is_err() || rc.is_ok_and(|s| !s.success()) {
let mut stdout = String::new();
proc.stdout
.unwrap()
.read_to_string(&mut stdout)
.expect("Failed to read stdout");
let mut stderr = String::new();
proc.stderr
.unwrap()
.read_to_string(&mut stderr)
.expect("Failed to read stderr");
panic!(
"Failed to compile {file_name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}"
);
}
}
}
}

111
src/apu.rs Normal file
View File

@@ -0,0 +1,111 @@
use tracing::debug;
bitfield::bitfield! {
pub struct DutyVol(u8);
impl Debug;
duty, set_duty: 7, 6;
r#loop, set_loop: 5;
const_vol, set_const_vol: 4;
volume, set_volume: 3, 0;
}
bitfield::bitfield! {
pub struct Sweep(u8);
impl Debug;
enable, set_enable: 7;
period, set_period: 6, 4;
negate, set_negate: 3;
shift, set_shift: 2, 0;
}
bitfield::bitfield! {
pub struct LengthTimerHigh(u8);
impl Debug;
length, set_length: 7, 3;
timer_high, set_timer_high: 2, 0;
}
struct PulseChannel {
enabled: bool,
}
bitfield::bitfield! {
pub struct CounterLoad(u8);
impl Debug;
halt, set_halt: 7;
value, set_value: 6, 0;
}
struct TriangleChannel {
enabled: bool,
}
struct NoiseChannel {
enabled: bool,
}
struct DeltaChannel {
enabled: bool,
}
pub struct APU {
pulse_1: PulseChannel,
pulse_2: PulseChannel,
triangle: TriangleChannel,
noise: NoiseChannel,
dmc: DeltaChannel,
frame_counter: u8,
}
impl std::fmt::Debug for APU {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(
// f,
// "PPU: f {}, s {}, p {}",
// self.frame_count, self.scanline, self.pixel
// )
Ok(())
}
}
impl APU {
pub fn init() -> Self {
Self {
pulse_1: PulseChannel { enabled: false },
pulse_2: PulseChannel { enabled: false },
triangle: TriangleChannel { enabled: false },
noise: NoiseChannel { enabled: false },
dmc: DeltaChannel { enabled: false },
frame_counter: 0,
}
}
pub fn read_reg(&mut self, offset: u16) -> u8 {
match offset {
_ => panic!("No register at {:X}", offset),
}
}
pub fn write_reg(&mut self, offset: u16, val: u8) {
match offset {
0x15 => {
self.dmc.enabled = val & 0b0001_0000 != 0;
self.noise.enabled = val & 0b0000_1000 != 0;
self.triangle.enabled = val & 0b0000_0100 != 0;
self.pulse_2.enabled = val & 0b0000_0010 != 0;
self.pulse_1.enabled = val & 0b0000_0001 != 0;
}
0x11 => {
// TODO: load dmc counter with (val & 7F)
}
_ => panic!("No register at {:X}", offset),
}
}
pub fn run_one_clock_cycle(&mut self) -> bool {
false
}
pub fn nmi_waiting(&mut self) -> bool {
false
}
pub fn irq_waiting(&mut self) -> bool {
// TODO: implement logic
false
}
}

14
src/controllers.rs Normal file
View File

@@ -0,0 +1,14 @@
pub struct Controllers {}
impl Controllers {
pub fn init() -> Self {
Self {}
}
pub fn read_joy1(&mut self) -> u8 {
0
}
pub fn read_joy2(&mut self) -> u8 {
0
}
pub fn write_joy_strobe(&mut self, val: u8) { }
}

944
src/header_menu.rs Normal file
View File

@@ -0,0 +1,944 @@
//! Pick lists display a dropdown list of selectable options.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::pick_list;
//!
//! struct State {
//! favorite: Option<Fruit>,
//! }
//!
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
//! enum Fruit {
//! Apple,
//! Orange,
//! Strawberry,
//! Tomato,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! FruitSelected(Fruit),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! let fruits = [
//! Fruit::Apple,
//! Fruit::Orange,
//! Fruit::Strawberry,
//! Fruit::Tomato,
//! ];
//!
//! pick_list(
//! fruits,
//! state.favorite,
//! Message::FruitSelected,
//! )
//! .placeholder("Select your favorite fruit...")
//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::FruitSelected(fruit) => {
//! state.favorite = Some(fruit);
//! }
//! }
//! }
//!
//! impl std::fmt::Display for Fruit {
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
//! f.write_str(match self {
//! Self::Apple => "Apple",
//! Self::Orange => "Orange",
//! Self::Strawberry => "Strawberry",
//! Self::Tomato => "Tomato",
//! })
//! }
//! }
//! ```
use iced::alignment;
use iced::event::{self, Event};
use iced::keyboard;
// use iced::layout;
use iced::mouse;
use iced::overlay;
// use iced::renderer;
// use iced::text::paragraph;
// use iced::text::{self, Text};
use iced::touch;
// use iced::widget::tree::{self, Tree};
use iced::overlay::menu::{self, Menu};
use iced::{
Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle, Size, Theme,
Vector,
};
use iced_graphics::core::text::paragraph;
use iced_graphics::core::widget::{Tree, tree};
use iced_graphics::core::{Clipboard, Layout, Shell, Text, Widget, layout, renderer, text};
use std::borrow::Borrow;
use std::f32;
/// A widget for selecting a single value from a list of options.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::pick_list;
///
/// struct State {
/// favorite: Option<Fruit>,
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// enum Fruit {
/// Apple,
/// Orange,
/// Strawberry,
/// Tomato,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// FruitSelected(Fruit),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// let fruits = [
/// Fruit::Apple,
/// Fruit::Orange,
/// Fruit::Strawberry,
/// Fruit::Tomato,
/// ];
///
/// pick_list(
/// fruits,
/// state.favorite,
/// Message::FruitSelected,
/// )
/// .placeholder("Select your favorite fruit...")
/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::FruitSelected(fruit) => {
/// state.favorite = Some(fruit);
/// }
/// }
/// }
///
/// impl std::fmt::Display for Fruit {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.write_str(match self {
/// Self::Apple => "Apple",
/// Self::Orange => "Orange",
/// Self::Strawberry => "Strawberry",
/// Self::Tomato => "Tomato",
/// })
/// }
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct HeaderMenu<
'a,
T,
L,
// V,
Message,
Theme = iced::Theme,
Renderer = iced::Renderer,
> where
T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a,
// V: Borrow<T> + 'a,
Theme: Catalog,
Renderer: text::Renderer,
{
title: String,
on_select: Box<dyn Fn(T) -> Message + 'a>,
on_open: Option<Message>,
on_close: Option<Message>,
options: L,
placeholder: Option<String>,
width: Length,
padding: Padding,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>,
class: <Theme as Catalog>::Class<'a>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
}
impl<'a, T, L, Message, Theme, Renderer> HeaderMenu<'a, T, L, Message, Theme, Renderer>
where
T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a,
// V: Borrow<T> + 'a,
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
/// Creates a new [`PickList`] with the given list of options, the current
/// selected value, and the message to produce when an option is selected.
pub fn new(
title: impl Into<String>,
options: L,
// selected: Option<V>,
on_click: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
title: title.into(),
on_select: Box::new(on_click),
on_open: None,
on_close: None,
options,
placeholder: None,
// selected,
width: Length::Shrink,
padding: Padding::new(0.1),
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
font: None,
handle: Handle::default(),
class: <Theme as Catalog>::default(),
menu_class: <Theme as Catalog>::default_menu(),
}
}
/// Sets the placeholder of the [`PickList`].
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
/// Sets the width of the [`PickList`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the [`Padding`] of the [`PickList`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the text size of the [`PickList`].
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
self.text_size = Some(size.into());
self
}
/// Sets the text [`text::LineHeight`] of the [`PickList`].
pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`PickList`].
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the font of the [`PickList`].
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the [`Handle`] of the [`PickList`].
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
self.handle = handle;
self
}
/// Sets the message that will be produced when the [`PickList`] is opened.
pub fn on_open(mut self, on_open: Message) -> Self {
self.on_open = Some(on_open);
self
}
/// Sets the message that will be produced when the [`PickList`] is closed.
pub fn on_close(mut self, on_close: Message) -> Self {
self.on_close = Some(on_close);
self
}
/// Sets the style of the [`PickList`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style of the [`Menu`].
#[must_use]
pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
where
<Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
{
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
self
}
}
impl<'a, T, L, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for HeaderMenu<'a, T, L, Message, Theme, Renderer>
where
T: Clone + ToString + PartialEq + 'a,
L: Borrow<[T]>,
// V: Borrow<T>,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<Renderer::Paragraph>::new())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
let options = self.options.borrow();
state.options.resize_with(options.len(), Default::default);
let option_text = Text {
content: "",
bounds: Size::new(
f32::INFINITY,
self.text_line_height.to_absolute(text_size).into(),
),
size: text_size,
line_height: self.text_line_height,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = &self.placeholder {
state.placeholder.update(Text {
content: placeholder,
..option_text
});
}
let max_width = match self.width {
Length::Shrink => {
let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
f32::max(width, paragraph.min_width())
});
labels_width.max(
self.placeholder
.as_ref()
.map(|_| state.placeholder.min_width())
.unwrap_or(0.0),
)
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
max_width + text_size.0 + self.padding.left,
f32::from(self.text_line_height.to_absolute(text_size)),
);
limits
.width(self.width)
.shrink(self.padding)
.resolve(self.width, Length::Shrink, intrinsic)
.expand(self.padding)
};
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside its
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
if let Some(on_close) = &self.on_close {
shell.publish(on_close.clone());
}
event::Status::Captured
} else if cursor.is_over(layout.bounds()) {
// let selected = self.selected.as_ref().map(Borrow::borrow);
state.is_open = true;
// state.hovered_option = self
// .options
// .borrow()
// .iter()
// .position(|option| Some(option) == selected);
if let Some(on_open) = &self.on_open {
shell.publish(on_open.clone());
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let options = self.options.borrow();
let selected = None; // self.selected.as_ref().map(Borrow::borrow);
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((self.on_select)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
// let selected = None; // self.selected.as_ref().map(Borrow::borrow);
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
// let is_selected = selected.is_some();
let status = if state.is_open {
Status::Opened
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
};
let style = Catalog::style(theme, &self.class, status);
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
..renderer::Quad::default()
},
style.background,
);
let handle = match &self.handle {
Handle::Arrow { size } => Some((
Renderer::ICON_FONT,
Renderer::ARROW_DOWN_ICON,
*size,
text::LineHeight::default(),
text::Shaping::Basic,
)),
Handle::Static(Icon {
font,
code_point,
size,
line_height,
shaping,
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state.is_open {
Some((
open.font,
open.code_point,
open.size,
open.line_height,
open.shaping,
))
} else {
Some((
closed.font,
closed.code_point,
closed.size,
closed.line_height,
closed.shaping,
))
}
}
Handle::None => None,
};
if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: code_point.to_string(),
size,
line_height,
font,
bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
bounds.center_y(),
),
style.text_color,
*viewport,
);
}
// let label = None; // selected.map(ToString::to_string);
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: self.title.clone(),
size: text_size,
line_height: self.text_line_height,
font,
bounds: Size::new(
bounds.width - self.padding.horizontal(),
f32::from(self.text_line_height.to_absolute(text_size)),
),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
// if is_selected {
// style.text_color
// } else {
style.placeholder_color,
// },
*viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
if state.is_open {
let bounds = layout.bounds();
let on_select = &self.on_select;
let mut menu = Menu::new(
&mut state.menu,
self.options.borrow(),
&mut state.hovered_option,
|option| {
state.is_open = false;
(on_select)(option)
},
None,
&self.menu_class,
)
.width(bounds.width)
.padding(self.padding)
.font(font)
.text_shaping(self.text_shaping);
if let Some(text_size) = self.text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position() + translation, bounds.height))
} else {
None
}
}
}
impl<'a, T, L, Message, Theme, Renderer> From<HeaderMenu<'a, T, L, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
T: Clone + ToString + PartialEq + 'a,
L: Borrow<[T]> + 'a,
// V: Borrow<T> + 'a,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(pick_list: HeaderMenu<'a, T, L, Message, Theme, Renderer>) -> Self {
Self::new(pick_list)
}
}
#[derive(Debug)]
struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
options: Vec<paragraph::Plain<P>>,
placeholder: paragraph::Plain<P>,
}
impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`] for a [`PickList`].
fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
options: Vec::new(),
placeholder: paragraph::Plain::default(),
}
}
}
impl<P: text::Paragraph> Default for State<P> {
fn default() -> Self {
Self::new()
}
}
/// The handle to the right side of the [`PickList`].
#[derive(Debug, Clone, PartialEq)]
pub enum Handle<Font> {
/// Displays an arrow icon (▼).
///
/// This is the default.
Arrow {
/// Font size of the content.
size: Option<Pixels>,
},
/// A custom static handle.
Static(Icon<Font>),
/// A custom dynamic handle.
Dynamic {
/// The [`Icon`] used when [`PickList`] is closed.
closed: Icon<Font>,
/// The [`Icon`] used when [`PickList`] is open.
open: Icon<Font>,
},
/// No handle will be shown.
None,
}
impl<Font> Default for Handle<Font> {
fn default() -> Self {
Self::Arrow { size: None }
}
}
/// The icon of a [`Handle`].
#[derive(Debug, Clone, PartialEq)]
pub struct Icon<Font> {
/// Font that will be used to display the `code_point`,
pub font: Font,
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
pub size: Option<Pixels>,
/// Line height of the content.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
pub shaping: text::Shaping,
}
/// The possible status of a [`PickList`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`PickList`] can be interacted with.
Active,
/// The [`PickList`] is being hovered.
Hovered,
/// The [`PickList`] is open.
Opened,
}
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
pub placeholder_color: Color,
/// The handle [`Color`] of the pick list.
pub handle_color: Color,
/// The [`Background`] of the pick list.
pub background: Background,
/// The [`Border`] of the pick list.
pub border: Border,
}
/// The theme catalog of a [`PickList`].
pub trait Catalog: menu::Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> <Self as Catalog>::Class<'a>;
/// The default class for the menu of the [`PickList`].
fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
<Self as menu::Catalog>::default()
}
/// The [`Style`] of a class with the given status.
fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`PickList`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> StyleFn<'a, Self> {
Box::new(default)
}
fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
class(self, status)
}
}
/// The default style of the field of a [`PickList`].
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let active = Style {
text_color: palette.background.weak.text,
background: palette.background.weak.color.into(),
placeholder_color: palette.background.strong.color,
handle_color: palette.background.weak.text,
border: Border {
radius: 2.0.into(),
width: 1.0,
color: palette.background.strong.color,
},
};
match status {
Status::Active => active,
Status::Hovered | Status::Opened => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
}
}
/// Creates a new [`PickList`].
///
/// Pick lists display a dropdown list of selectable options.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::pick_list;
///
/// struct State {
/// favorite: Option<Fruit>,
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// enum Fruit {
/// Apple,
/// Orange,
/// Strawberry,
/// Tomato,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// FruitSelected(Fruit),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// let fruits = [
/// Fruit::Apple,
/// Fruit::Orange,
/// Fruit::Strawberry,
/// Fruit::Tomato,
/// ];
///
/// pick_list(
/// fruits,
/// state.favorite,
/// Message::FruitSelected,
/// )
/// .placeholder("Select your favorite fruit...")
/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::FruitSelected(fruit) => {
/// state.favorite = Some(fruit);
/// }
/// }
/// }
///
/// impl std::fmt::Display for Fruit {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.write_str(match self {
/// Self::Apple => "Apple",
/// Self::Orange => "Orange",
/// Self::Strawberry => "Strawberry",
/// Self::Tomato => "Tomato",
/// })
/// }
/// }
/// ```
pub fn header_menu<'a, T, L, Message, Theme, Renderer>(
title: impl Into<String>,
options: L,
on_selected: impl Fn(T) -> Message + 'a,
) -> HeaderMenu<'a, T, L, Message, Theme, Renderer>
where
T: ToString + PartialEq + Clone + 'a,
L: Borrow<[T]> + 'a,
Message: Clone,
Theme: Catalog + overlay::menu::Catalog,
Renderer: text::Renderer,
{
HeaderMenu::new(title, options, on_selected)
}

57
src/hex_view.rs Normal file
View File

@@ -0,0 +1,57 @@
use std::fmt;
use iced::{widget::{column, lazy, row, text}, Element, Font, Length::Fill, Task};
pub trait Memory {
fn peek(&self, val: u16) -> Option<u8>;
fn edit_ver(&self) -> usize;
}
#[derive(Debug, Clone)]
pub enum HexEvent {}
pub struct HexView {
}
struct Val(Option<u8>);
impl fmt::Display for Val {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(val) = self.0 {
write!(f, "{:02X}", val)
} else {
write!(f, "XX")
}
}
}
impl HexView {
pub fn new() -> Self {
Self {}
}
pub fn render<'a>(&self, mem: &'a impl Memory) -> Element<'a, HexEvent> {
struct Row<'a, M>(u16, &'a M);
impl<M: Memory> fmt::Display for Row<'_, M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Val(self.1.peek(self.0)))?;
for i in 1..16 {
write!(f, " {}", Val(self.1.peek(self.0 + i)))?;
}
Ok(())
}
}
column![
text!("Hex view"),
iced::widget::scrollable(lazy(mem.edit_ver(), |_| column([
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F").font(Font::MONOSPACE).into()
].into_iter().chain((0..u16::MAX).step_by(16).map(|off| {
text!(" {off:04X} | {}", Row(off, mem)).font(Font::MONOSPACE).into()
}))))).width(Fill),
].width(Fill).into()
}
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
todo!()
}
}

1574
src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

310
src/main.rs Normal file
View File

@@ -0,0 +1,310 @@
use std::{
collections::HashMap,
fmt,
io::{Read, stdin},
};
use iced::{
Color, Element, Font,
Length::Fill,
Point, Renderer, Size, Subscription, Task, Theme, Vector,
futures::io::Window,
mouse,
widget::{
Canvas, button,
canvas::{Frame, Program},
column, container, row, text,
},
window::{self, Id, Settings},
};
use nes_emu::{
NES, PPU, RenderBuffer,
header_menu::header_menu,
hex_view::{self, HexEvent, HexView},
};
use tracing::{debug, info};
use tracing_subscriber::EnvFilter;
extern crate nes_emu;
fn main() -> Result<(), iced::Error> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
iced::daemon(Emulator::title, Emulator::update, Emulator::view)
.subscription(Emulator::subscriptions)
.theme(|_, _| Theme::Dark)
.run_with(Emulator::new)
// iced::application(title, Emulator::update, Emulator::view)
// .subscription(Emulator::subscriptions)
// .theme(|_| Theme::Dark)
// .centered()
// .run()
}
enum MemoryTy {
Cpu,
}
enum WindowType {
Main,
Memory(MemoryTy, HexView),
TileMap,
TileViewer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HeaderButton {
OpenMemory,
OpenTileMap,
OpenTileViewer,
Reset,
PowerCycle,
}
impl fmt::Display for HeaderButton {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OpenMemory => write!(f, "Open Memory Viewer"),
Self::OpenTileMap => write!(f, "Open TileMap Viewer"),
Self::OpenTileViewer => write!(f, "Open Tile Viewer"),
Self::Reset => write!(f, "Reset"),
Self::PowerCycle => write!(f, "Power Cycle"),
}
}
}
struct Emulator {
nes: NES,
windows: HashMap<Id, WindowType>,
}
#[derive(Debug, Clone)]
enum Message {
Tick(usize),
Frame,
DMA,
CPU,
DebugInt,
WindowOpened(Id),
WindowClosed(Id),
Header(HeaderButton),
Hex(Id, HexEvent),
}
impl Emulator {
fn new() -> (Self, Task<Message>) {
let rom_file = concat!(env!("ROM_DIR"), "/", "read_write.nes");
// let rom_file = "./Super Mario Bros. (World).nes";
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::default());
(
Self {
nes,
windows: HashMap::from_iter([(win, WindowType::Main)]),
},
task.map(Message::WindowOpened),
)
}
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(),
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.map(Message::WindowOpened);
}
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::WindowOpened(id) => {
// Window
}
Message::WindowClosed(id) => {
if let Some(WindowType::Main) = self.windows.remove(&id) {
return iced::exit();
}
}
Message::Header(HeaderButton::OpenMemory) => {
return self.open(WindowType::Memory(MemoryTy::Cpu, HexView::new()));
}
Message::Header(HeaderButton::OpenTileMap) => {
return self.open(WindowType::TileMap);
}
Message::Header(HeaderButton::OpenTileViewer) => {
return self.open(WindowType::TileViewer);
}
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(); }
}
// self.image.0.clone_from(self.nes.image());
Task::none()
}
fn subscriptions(&self) -> Subscription<Message> {
window::close_events().map(Message::WindowClosed)
}
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(Fill).height(255. * 2.)),
self.cpu_state(),
self.controls(),
]
.height(Fill);
container(content).width(Fill).height(Fill).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::PatternTable(self.nes.ppu()))).width(Fill).height(Fill).into()
}
Some(WindowType::TileViewer) => {
container(Canvas::new(DbgImage::NameTable(self.nes.ppu()))).width(Fill).height(Fill).into()
}
_ => todo!(),
}
}
fn cpu_state(&self) -> Element<'_, Message> {
row![column![
// text!("Registers").font(Font::MONOSPACE),
text!("{:?}", self.nes).font(Font::MONOSPACE),
],]
.width(Fill)
.into()
}
fn controls(&self) -> Element<'_, Message> {
row![
button("Clock tick").on_press(Message::Tick(1)),
button("CPU tick").on_press(Message::CPU),
button("Next Frame").on_press(Message::Frame),
button("Next DMA").on_press(Message::DMA),
button("Next DBG").on_press(Message::DebugInt),
]
.width(Fill)
.into()
}
fn dropdowns(&self) -> Element<'_, Message> {
row![
header_menu(
"Console",
[
HeaderButton::Reset,
HeaderButton::PowerCycle,
],
Message::Header
),
header_menu(
"Debugging",
[
HeaderButton::OpenMemory,
HeaderButton::OpenTileMap,
HeaderButton::OpenTileViewer,
],
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,
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),
}
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),
}
vec![name_table_frame.into_geometry()]
}
}

163
src/mem.rs Normal file
View File

@@ -0,0 +1,163 @@
use crate::hex_view::Memory;
pub enum Value<'a, R> {
Value(u8),
Register { reg: &'a R, offset: u16 },
}
impl<R> Value<'_, R> {
pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 {
match self {
Value::Value(v) => v,
Value::Register { reg, offset } => f(reg, offset),
}
}
}
pub enum Data<R> {
RAM(Vec<u8>),
ROM(Vec<u8>),
Mirror(usize),
Reg(R),
// Disabled(),
}
pub struct Segment<R> {
name: &'static str,
position: u16,
size: u16,
mem: Data<R>,
}
impl<R> Segment<R> {
pub fn ram(name: &'static str, position: u16, size: u16) -> Self {
Self {
name,
position,
size,
mem: Data::RAM(vec![0u8; size as usize]),
}
}
pub fn rom(name: &'static str, position: u16, raw: &[u8]) -> Self {
Self {
name,
position,
size: raw.len() as u16,
mem: Data::ROM(Vec::from(raw)),
}
}
pub fn reg(name: &'static str, position: u16, size: u16, reg: R) -> Self {
Self {
name,
position,
size,
mem: Data::Reg(reg),
}
}
pub fn mirror(name: &'static str, position: u16, size: u16, of: usize) -> Self {
Self {
name,
position,
size,
mem: Data::Mirror(of),
}
}
}
pub struct MemoryMap<R> {
edit_ver: usize,
segments: Vec<Segment<R>>,
}
impl<R> MemoryMap<R> {
pub fn new(segments: Vec<Segment<R>>) -> Self {
Self { edit_ver: 0, segments }
}
pub fn read(&self, addr: u16) -> Value<'_, R> {
// self.edit_ver += 1;
for segment in &self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &segment.mem {
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
Data::ROM(items) => Value::Value(items[(addr - segment.position) as usize]),
Data::Reg(reg) => Value::Register {
reg,
offset: addr - segment.position,
},
Data::Mirror(index) => {
let offset = addr - segment.position;
let s = &self.segments[*index];
self.read(s.position + offset % s.size)
}
// Data::Disabled() => todo!(),
};
}
}
todo!("Open bus")
}
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&R, u16, u8)) {
self.edit_ver += 1;
for segment in &mut self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &mut segment.mem {
Data::RAM(items) => {
items[(addr - segment.position) as usize] = val;
}
Data::ROM(_items) => (),
Data::Reg(reg) => reg_fn(reg, addr - segment.position, val),
Data::Mirror(index) => {
let offset = addr - segment.position;
let index = *index;
let s = &self.segments[index];
self.write(s.position + offset % s.size, val, reg_fn)
}
// Data::Disabled() => todo!(),
};
}
}
todo!("Open bus")
}
pub fn clear(&mut self) {
for s in &mut self.segments {
match &mut s.mem {
Data::RAM(items) => items.fill(0),
_ => (),
}
}
}
pub(crate) fn rom(&self, idx: usize) -> Option<&[u8]> {
if let Some(Segment { mem: Data::ROM(val), .. }) = self.segments.get(idx) {
Some(val)
} else {
None
}
}
}
impl<R> Memory for MemoryMap<R> {
fn peek(&self, addr: u16) -> Option<u8> {
for segment in &self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &segment.mem {
Data::RAM(items) => Some(items[(addr - segment.position) as usize]),
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
Data::Reg(_) => None,
Data::Mirror(index) => {
let offset = addr - segment.position;
let s = &self.segments[*index];
self.peek(s.position + offset % s.size)
}
// Data::Disabled() => todo!(),
};
}
}
None
}
fn edit_ver(&self) -> usize {
self.edit_ver
}
}

495
src/ppu.rs Normal file
View File

@@ -0,0 +1,495 @@
use iced::{Point, Size, widget::canvas::Frame};
use iced_graphics::geometry::{Fill, Renderer};
use crate::{hex_view::Memory, mem::{MemoryMap, Segment}};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Into<Fill> for Color {
fn into(self) -> Fill {
iced::Color::from_rgb8(self.r, self.g, self.b).into()
}
}
pub struct RenderBuffer<const W: usize, const H: usize> {
buffer: Box<[Color]>,
}
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
pub fn empty() -> Self {
Self {
buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(),
}
}
pub fn write(&mut self, line: usize, pixel: usize, color: Color) {
assert!(line < H && pixel < W);
self.buffer[line * W + pixel] = color;
}
pub fn read(&self, line: usize, pixel: usize) -> Color {
assert!(line < H && pixel < W);
self.buffer[line * W + pixel]
}
pub fn clone_from(&mut self, other: &Self) {
self.buffer.copy_from_slice(&other.buffer);
}
}
pub(crate) enum PPUMMRegisters {
Palette,
}
pub struct OAM {
mem: Vec<u8>,
addr: u8,
}
enum BackgroundState {
NameTableBytePre,
NameTableByte,
AttrTableBytePre,
AttrTableByte,
PatternTableTileLowPre,
PatternTableTileLow,
PatternTableTileHighPre,
PatternTableTileHigh,
}
pub struct Background {
/// Current vram address, 15 bits
v: u16,
/// Temp vram address, 15 bits
t: u16,
/// Fine X control, 3 bits
x: u8,
/// Whether this is the first or second write to PPUSCROLL
/// When false, writes to x
w: bool,
/// When true, v is incremented by 32 after each read
vram_column: bool,
state: BackgroundState,
cur_shift_high: u8,
cur_shift_low: u8,
}
struct Mask {
grayscale: bool,
background_on_left_edge: bool,
sprites_on_left_edge: bool,
enable_background: bool,
enable_sprites: bool,
em_red: bool,
em_green: bool,
em_blue: bool,
}
pub struct PPU {
// registers: PPURegisters,
frame_count: usize,
nmi_enabled: bool,
nmi_waiting: bool,
even: bool,
scanline: usize,
pixel: usize,
mask: Mask,
vblank: bool,
pub(crate) memory: MemoryMap<PPUMMRegisters>,
background: Background,
oam: OAM,
pub render_buffer: RenderBuffer<256, 240>,
pub dbg_int: bool,
pub cycle: usize,
}
bitfield::bitfield! {
pub struct PPUStatus(u8);
impl Debug;
vblank, set_vblank: 7;
sprite0, set_sprite0: 6;
sprite_overflow, set_sprite_overflow: 5;
}
impl std::fmt::Debug for PPU {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PPU: f {}, s {}, p {}",
self.frame_count, self.scanline, self.pixel
)
}
}
impl PPU {
pub fn with_chr_rom(rom: &[u8]) -> Self {
Self {
cycle: 25,
dbg_int: false,
mask: Mask {
grayscale: false,
background_on_left_edge: false,
sprites_on_left_edge: false,
enable_background: false,
enable_sprites: false,
em_red: false,
em_green: false,
em_blue: false,
},
vblank: false,
frame_count: 0,
nmi_enabled: false,
nmi_waiting: false,
even: true, // ??
scanline: 0,
pixel: 25,
render_buffer: RenderBuffer::empty(),
memory: MemoryMap::new(vec![
Segment::rom("CHR ROM", 0x0000, rom),
Segment::ram("Internal VRAM", 0x2000, 0x1000),
Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1),
Segment::reg("Palette Control", 0x3F00, 0x0100, PPUMMRegisters::Palette),
]),
background: Background {
v: 0,
t: 0,
x: 0,
w: false,
vram_column: false,
state: BackgroundState::NameTableBytePre,
cur_shift_high: 0,
cur_shift_low: 0,
},
oam: OAM {
mem: vec![0u8; 256],
addr: 0,
},
}
}
pub fn reset(&mut self) {
*self = Self {
memory: std::mem::replace(&mut self.memory, MemoryMap::new(vec![])),
..Self::with_chr_rom(&[])
};
}
pub fn read_reg(&mut self, offset: u16) -> u8 {
match offset {
0 => panic!("ppuctrl is write-only"),
1 => panic!("ppumask is write-only"),
2 => {
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
self.vblank = false;
self.background.w = false;
tmp
}
3 => panic!("oamaddr is write-only"),
4 => self.oam.mem[self.oam.addr as usize],
5 => panic!("ppuscroll is write-only"),
6 => panic!("ppuaddr is write-only"),
7 => {
let val = self.memory.read(self.background.v).reg_map(|a, _| match a {
PPUMMRegisters::Palette => todo!(),
});
// if self.background
self.background.v = self
.background
.v
.wrapping_add(if self.background.vram_column { 32 } else { 1 });
val
}
// 7 => self.registers.data,
_ => panic!("No register at {:02X}", offset),
}
}
pub fn write_reg(&mut self, offset: u16, val: u8) {
match offset {
0x00 => {
self.nmi_enabled = val & 0b1000_0000 != 0;
self.background.t =
(self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
self.background.vram_column = val & 0b0000_0010 != 0;
// TODO: other control fields
}
0x01 => {
// self.dbg_int = true;
self.mask.grayscale = val & 0b0000_0001 != 0;
self.mask.background_on_left_edge = val & 0b0000_0010 != 0;
self.mask.sprites_on_left_edge = val & 0b0000_0100 != 0;
self.mask.enable_background = val & 0b0000_1000 != 0;
self.mask.enable_background = val & 0b0001_0000 != 0;
self.mask.em_red = val & 0b0010_0000 != 0;
self.mask.em_green = val & 0b0100_0000 != 0;
self.mask.em_blue = val & 0b1000_0000 != 0;
// todo!("Mask: {:02X}", val)
}
0x02 => {
todo!("Unable to write to PPU status")
// TODO: ppu status
}
0x03 => self.oam.addr = val,
0x04 => {
self.oam.mem[self.oam.addr as usize] = val;
self.oam.addr = self.oam.addr.wrapping_add(1);
}
0x05 => {
if self.background.w {
// Is y scroll
self.background.t = (self.background.t & 0b0000_1100_0001_1111)
| (((val as u16) << 2) & 0b0000_0011_1110_0000)
| (((val as u16) << 12) & 0b0111_0000_0000_0000);
self.background.w = false;
} else {
// Is x scroll
self.background.x = val & 0b111; // Lowest three bits
self.background.t =
(self.background.t & 0b0111_1111_1110_0000) | (val as u16 >> 3);
self.background.w = true;
}
}
0x06 => {
// TODO: ppu addr
if self.background.w {
self.background.v =
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
self.background.w = true;
} else {
self.background.v =
u16::from_le_bytes([self.background.v.to_le_bytes()[0], val & 0b0011_1111]);
self.background.w = true;
}
// todo!("PPUADDR write")
}
0x07 => {
// TODO: ppu data
}
_ => panic!("No register at {:02X}", offset),
}
// TODO: use data in PPU
}
// pub fn write_oamdma(&mut self, val: u8) {
// // TODO: OAM high addr
// }
pub fn run_one_clock_cycle(&mut self) -> bool {
self.cycle += 1;
self.pixel += 1;
if self.scanline == 261 && (self.pixel == 341 || (self.pixel == 340 && self.even)) {
self.scanline = 0;
self.pixel = 0;
self.even = !self.even;
}
if self.pixel == 341 {
self.pixel = 0;
self.scanline += 1;
}
if self.mask.enable_background || self.mask.enable_sprites {
if self.pixel == 257 {
self.background.v = (self.background.v & 0b0111_1011_1110_0000)
| (self.background.t & 0b0000_0100_0001_1111);
}
if self.pixel == 280 && self.scanline == 260 {
self.background.v = (self.background.v & 0b0000_0100_0001_1111)
| (self.background.t & 0b0111_1011_1110_0000);
}
if self.scanline < 240 || self.scanline == 261 {
// TODO
if self.pixel == 0 {
// self.dbg_int = true;
// idle cycle
} else if self.pixel < 257 {
// self.dbg_int = true;
if self.scanline < 240 {
self.render_buffer.write(
self.scanline,
self.pixel - 1,
Color { r: 255, g: 0, b: 0 },
); // TODO: this should come from shift registers
}
self.background.state = match self.background.state {
BackgroundState::NameTableByte => {
// TODO: Fetch name table byte
BackgroundState::AttrTableBytePre
}
BackgroundState::AttrTableByte => {
// TODO: Fetch attr table byte
BackgroundState::PatternTableTileLowPre
}
BackgroundState::PatternTableTileLow => {
// TODO: Fetch
BackgroundState::PatternTableTileHighPre
}
BackgroundState::PatternTableTileHigh => {
// TODO: Fetch
BackgroundState::NameTableBytePre
}
BackgroundState::NameTableBytePre => BackgroundState::NameTableByte,
BackgroundState::AttrTableBytePre => BackgroundState::AttrTableByte,
BackgroundState::PatternTableTileLowPre => {
BackgroundState::PatternTableTileLow
}
BackgroundState::PatternTableTileHighPre => {
BackgroundState::PatternTableTileHigh
}
};
} else {
// TODO: Sprite fetches
}
}
}
if self.scanline == 241 && self.pixel == 1 {
self.vblank = true;
self.nmi_waiting = self.nmi_enabled;
self.frame_count += 1;
self.background.state = BackgroundState::NameTableBytePre;
return true;
}
return false;
}
pub fn nmi_waiting(&mut self) -> bool {
if self.nmi_waiting {
self.nmi_waiting = false;
return true;
} else {
return false;
}
}
pub fn irq_waiting(&mut self) -> bool {
false
}
pub fn render_name_table<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..280 / 8 {
for x in 0..512 / 8 {
let name = self.memory.peek(x + y * 512 / 8 + 0x2000).unwrap() as u16 * 16;
for y_off in 0..8 {
let low = self.memory.peek(name + y_off).unwrap();
let high = self.memory.peek(name + y_off + 8).unwrap();
for bit in (0..8).rev() {
frame.fill_rectangle(
Point::new(x as f32 * 8. + bit as f32, y as f32 * 8. + y_off as f32),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color { r: 64, g: 64, b: 64 },
(false, true) => Color { r: 128, g: 128, b: 128 },
(true, true) => Color { r: 255, g: 255, b: 255 },
}
);
}
}
// for
// let pat = self.memory.peek();
}
}
}
pub fn render_pattern_tables<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = self.memory.peek(name + y_off).unwrap();
let high = self.memory.peek(name + y_off + 8).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color { r: 64, g: 64, b: 64 },
(false, true) => Color { r: 128, g: 128, b: 128 },
(true, true) => Color { r: 255, g: 255, b: 255 },
}
);
}
}
}
}
for y in 0..16 {
for x in 0..16 {
let name = (y * 16 + x) * 16;
for y_off in 0..8 {
let low = self.memory.peek(name + y_off + 0x1000).unwrap();
let high = self.memory.peek(name + y_off + 8 + 0x1000).unwrap();
for bit in 0..8 {
frame.fill_rectangle(
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32 + 130.),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => Color { r: 0, g: 0, b: 0 },
(true, false) => Color { r: 64, g: 64, b: 64 },
(false, true) => Color { r: 128, g: 128, b: 128 },
(true, true) => Color { r: 255, g: 255, b: 255 },
}
);
}
}
// for
// let pat = self.memory.peek();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ppu_registers() {
let mut ppu = PPU::with_chr_rom(&[0u8; 8192]);
assert_eq!(ppu.background.v, 0);
assert_eq!(ppu.background.t, 0);
assert_eq!(ppu.background.x, 0);
assert_eq!(ppu.background.w, false);
ppu.write_reg(0, 0);
assert_eq!(ppu.background.v, 0);
ppu.write_reg(0, 0b11);
assert_eq!(
ppu.background.t, 0b0001100_00000000,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.w, false);
ppu.write_reg(5, 0x7D);
assert_eq!(
ppu.background.t, 0b0001100_00001111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.write_reg(5, 0x5E);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, false);
ppu.write_reg(5, 0x7D);
assert_eq!(
ppu.background.t, 0b1101101_01101111,
"Actual: {:016b}",
ppu.background.t
);
assert_eq!(ppu.background.x, 0b101);
assert_eq!(ppu.background.w, true);
ppu.read_reg(2);
assert_eq!(ppu.background.w, false);
}
}

BIN
src/test_roms/Sprites.pcx Normal file

Binary file not shown.

BIN
src/test_roms/Tiles.pcx Normal file

Binary file not shown.

View File

@@ -0,0 +1,26 @@
; FINAL = ""
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM
.inesmir 0 ; background mirroring, horizontal
.org $8000
RESET:
sed
hlt
ERROR_:
hlt
IGNORE:
rti
.org $FFFA ; Interrupt vectors go here:
.word IGNORE ; NMI
.word RESET ; Reset
.word IGNORE; IRQ
;;;; NESASM COMPILER STUFF, ADDING THE PATTERN DATA ;;;;
.incbin "Sprites.pcx"
.incbin "Tiles.pcx"

41
src/test_roms/mod.rs Normal file
View File

@@ -0,0 +1,41 @@
use crate::{NES, hex_view::Memory};
macro_rules! rom_test {
($name:ident, $rom:literal, |$nes:ident| $eval:expr) => {
#[test]
fn $name() {
let rom_file = concat!(env!("ROM_DIR"), "/", $rom);
println!("{}: {}", stringify!($name), rom_file);
let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object");
$nes.reset_and_run_with_timeout(1000000);
println!("Final: {:?}", $nes);
$eval
}
};
}
rom_test!(basic_cpu, "basic-cpu.nes", |nes| {
assert_eq!(nes.last_instruction, "0x8001 HLT :2 []");
// Off by one from Mesen, since Mesen doesn't count the clock cycle attempting to execute the 'invalid' opcode
assert_eq!(nes.cycle, 11);
// This is off by one from Mesen, because Mesen is left pointing at the 'invalid' opcode
assert_eq!(nes.cpu.pc, 0x8002);
assert_eq!(nes.cpu.sp, 0xFD);
// assert_eq!(nes.cpu.a, 0x00);
// assert_eq!(nes.cpu.x, 0x00);
// assert_eq!(nes.cpu.y, 0x00);
});
rom_test!(read_write, "read_write.nes", |nes| {
assert_eq!(nes.last_instruction, "0x8011 HLT :2 []");
assert_eq!(nes.cycle, 31);
assert_eq!(nes.cpu.pc, 0x8012);
assert_eq!(nes.cpu.sp, 0xFD);
assert_eq!(nes.cpu.a, 0xAA);
assert_eq!(nes.cpu.x, 0xAA);
assert_eq!(nes.cpu.y, 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0000).unwrap(), 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0001).unwrap(), 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0002).unwrap(), 0xAA);
});

View File

@@ -0,0 +1,31 @@
; FINAL = ""
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM
.inesmir 0 ; background mirroring, horizontal
.org $8000
RESET:
lda #$aa
sta $0000
ldx $0000
ldy $0000
stx $0001
sty $0002
hlt
ERROR_:
hlt
IGNORE:
rti
.org $FFFA ; Interrupt vectors go here:
.word IGNORE ; NMI
.word RESET ; Reset
.word IGNORE; IRQ
;;;; NESASM COMPILER STUFF, ADDING THE PATTERN DATA ;;;;
.incbin "Sprites.pcx"
.incbin "Tiles.pcx"