Compare commits

...

2 Commits

Author SHA1 Message Date
af770d232c Finally find (& fix) bug in BIT instructions
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 26s
- BIT not longer ANDs the A register
- I now a pretty good debug view for debugging the CPU
- I wrote a number_input element for iced
- I upgraded to iced 0.14
- I added images for play and pause
- The debug log now displays in the debug view
2025-12-14 13:10:57 -06:00
fecef26e2f Update tests with basic init 2025-12-13 20:29:52 -06:00
21 changed files with 19587 additions and 1299 deletions

2235
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,10 @@ 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"] }
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
iced = { path = "../iced", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
# iced_graphics = { version = "0.14.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"] }

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
<path d="M5.74609 3C4.7796 3 3.99609 3.7835 3.99609 4.75V19.25C3.99609 20.2165 4.7796 21 5.74609 21H9.24609C10.2126 21 10.9961 20.2165 10.9961 19.25V4.75C10.9961 3.7835 10.2126 3 9.24609 3H5.74609ZM14.7461 3C13.7796 3 12.9961 3.7835 12.9961 4.75V19.25C12.9961 20.2165 13.7796 21 14.7461 21H18.2461C19.2126 21 19.9961 20.2165 19.9961 19.25V4.75C19.9961 3.7835 19.2126 3 18.2461 3H14.7461Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
<path d="M5 5.27368C5 3.56682 6.82609 2.48151 8.32538 3.2973L20.687 10.0235C22.2531 10.8756 22.2531 13.124 20.687 13.9762L8.32538 20.7024C6.82609 21.5181 5 20.4328 5 18.726V5.27368Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

View File

@@ -91,6 +91,10 @@ impl APU {
self.pulse_2.enabled = val & 0b0000_0010 != 0;
self.pulse_1.enabled = val & 0b0000_0001 != 0;
}
0x10 => {
assert_eq!(val, 0x00);
// TODO: implement this value
}
0x11 => {
// TODO: load dmc counter with (val & 7F)
}
@@ -101,9 +105,15 @@ impl APU {
pub fn run_one_clock_cycle(&mut self) -> bool {
false
}
pub fn peek_nmi(&self) -> bool {
false
}
pub fn nmi_waiting(&mut self) -> bool {
false
}
pub fn peek_irq(&self) -> bool {
false
}
pub fn irq_waiting(&mut self) -> bool {
// TODO: implement logic
false

71
src/debug.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::num::NonZeroUsize;
pub struct DebugLog {
current: String,
history: Vec<String>,
max_history: Option<NonZeroUsize>,
pos: usize,
}
impl DebugLog {
pub fn new() -> Self {
Self {
current: String::new(),
history: vec![],
max_history: None,
pos: 0,
}
}
pub fn rotate(&mut self) {
// if self.current.len() > 500 {
let mut rot = std::mem::take(&mut self.current);
self.current = rot.split_off(rot.rfind('\n').unwrap_or(rot.len()));
// if let Some(max) = self.max_history {
// if self.history.len() < max.into() {
// self.history.extend(rot.lines().map(|s| s.to_owned()));
// } else {
// self.history[self.pos] = rot;
// self.pos = (self.pos + 1) % max.get();
// }
// } else {
// self.history.push(rot);
self.history.extend(rot.lines().map(|s| s.to_owned()));
// }
// }
}
// pub fn current(&self) -> &str {
// &self.current
// }
pub fn history(&self) -> &[String] {
&self.history[self.history.len().saturating_sub(100)..]
}
}
impl std::fmt::Write for DebugLog {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let tmp = self.current.write_str(s);
self.rotate();
tmp
}
fn write_char(&mut self, c: char) -> std::fmt::Result {
let tmp = self.current.write_char(c);
self.rotate();
tmp
}
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
let tmp = self.current.write_fmt(args);
self.rotate();
tmp
}
}
impl Default for DebugLog {
fn default() -> Self {
Self::new()
}
}

232
src/debugger.rs Normal file
View File

@@ -0,0 +1,232 @@
use iced::{
Element,
Length::Fill,
widget::{
self, button, checkbox, column, container::bordered_box, image, number_input, row,
scrollable, text,
},
};
use crate::{CycleResult, NES};
#[derive(Debug, Clone)]
pub struct DebuggerState {
ppu_cycles: usize,
cpu_cycles: usize,
instructions: usize,
scan_lines: usize,
to_scan_line: usize,
frames: usize,
}
#[derive(Debug, Clone)]
pub enum DebuggerMessage {
Run,
Pause,
SetPPUCycles(usize),
RunPPUCycles,
SetCPUCycles(usize),
RunCPUCycles,
SetInstructions(usize),
RunInstructions,
SetScanLines(usize),
RunScanLines,
SetToScanLine(usize),
RunToScanLine,
SetFrames(usize),
RunFrames,
}
impl DebuggerState {
pub fn new() -> Self {
Self {
ppu_cycles: 1,
cpu_cycles: 1,
instructions: 1,
scan_lines: 1,
to_scan_line: 1,
frames: 1,
// cpu_cycles: 1,
}
}
pub fn view<'s>(&'s self, nes: &'s NES) -> Element<'s, DebuggerMessage> {
column![
row![
button(image("./images/ic_fluent_play_24_filled.png"))
.on_press(DebuggerMessage::Run),
button(image("./images/ic_fluent_pause_24_filled.png"))
.on_press(DebuggerMessage::Pause),
],
iced::widget::rule::horizontal(2.),
row![column![
text("Status"),
row![
labelled("A:", text(format!("{:02X}", nes.cpu.a))),
labelled("X:", text(format!("{:02X}", nes.cpu.x))),
labelled("Y:", text(format!("{:02X}", nes.cpu.y))),
labelled("PC:", text(format!("{:04X}", nes.cpu.pc))),
labelled("Cycle:", text(format!("{}", nes.cycle))),
labelled("SP:", text(format!("{:02X}", nes.cpu.sp))),
]
.spacing(5.),
row![
labelled("P:", text(format!("{:02X}", nes.cpu.status.0))),
labelled_box("Carry", nes.cpu.status.carry()),
labelled_box("Zero", nes.cpu.status.zero()),
labelled_box("Interrupt", nes.cpu.status.interrupt_disable()),
labelled_box("--", false),
labelled_box("--", false),
labelled_box("Overflow", nes.cpu.status.overflow()),
labelled_box("Negative", nes.cpu.status.negative()),
]
.spacing(5.),
row![
text("IRQs:"),
labelled_box("NMI", nes.peek_nmi()),
labelled_box("Cart", false),
labelled_box("Frame Counter", false),
labelled_box("DMC", false),
]
.spacing(5.),
row![
column![
labelled("Cycle", text(nes.ppu.pixel)),
labelled("Scanline", text(nes.ppu.scanline)),
labelled("PPU Cycle", text(nes.ppu.cycle)),
],
column![
labelled_box("Sprite 0 Hit", false),
labelled_box("Sprite 0 Overflow", false),
labelled_box("Vertical Blank", nes.ppu.vblank),
labelled_box("Write Toggle", false),
labelled_box("", false),
labelled_box("Large Sprites", false),
labelled_box("Vertical Write", false),
labelled_box("NMI on VBlank", false),
labelled_box("BG at $1000", false),
labelled_box("Sprites at $1000", false),
],
column![
run_type(
"PPU Cycles:",
self.ppu_cycles,
DebuggerMessage::SetPPUCycles,
DebuggerMessage::RunPPUCycles
),
run_type(
"CPU Cycles:",
self.cpu_cycles,
DebuggerMessage::SetCPUCycles,
DebuggerMessage::RunCPUCycles
),
run_type(
"Instructions:",
self.instructions,
DebuggerMessage::SetInstructions,
DebuggerMessage::RunInstructions
),
run_type(
"Scanlines:",
self.scan_lines,
DebuggerMessage::SetScanLines,
DebuggerMessage::RunScanLines
),
run_type(
"To Scanline:",
self.to_scan_line,
DebuggerMessage::SetToScanLine,
DebuggerMessage::RunToScanLine
),
run_type(
"Frames:",
self.frames,
DebuggerMessage::SetFrames,
DebuggerMessage::RunFrames
),
],
]
.spacing(5.),
scrollable(column(
nes.debug_log()
.history()
.into_iter()
.rev()
.map(|s| text(s).line_height(0.9).into())
).spacing(0))
.width(Fill),
],],
]
.width(Fill)
.height(Fill)
.into()
}
fn run_n_clock_cycles(nes: &mut NES, n: usize) {
for _ in 0..n {
nes.run_one_clock_cycle();
}
}
fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool) {
loop {
if f(nes.run_one_clock_cycle(), nes) {
break;
}
}
}
pub fn update(&mut self, message: DebuggerMessage, nes: &mut NES) {
match message {
DebuggerMessage::SetPPUCycles(n) => self.ppu_cycles = n,
DebuggerMessage::SetCPUCycles(n) => self.cpu_cycles = n,
DebuggerMessage::SetInstructions(n) => self.instructions = n,
DebuggerMessage::SetScanLines(n) => self.scan_lines = n,
DebuggerMessage::SetToScanLine(n) => self.to_scan_line = n,
DebuggerMessage::SetFrames(n) => self.frames = n,
DebuggerMessage::RunPPUCycles => Self::run_n_clock_cycles(nes, self.ppu_cycles),
DebuggerMessage::RunCPUCycles => Self::run_n_clock_cycles(nes, self.cpu_cycles * 3),
DebuggerMessage::RunInstructions => Self::run_until(nes, |c, _| c.cpu_exec),
DebuggerMessage::RunScanLines => Self::run_n_clock_cycles(nes, self.scan_lines * 341),
DebuggerMessage::RunToScanLine => {
Self::run_until(nes, |_, n| n.ppu.scanline == self.to_scan_line)
}
DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 261),
DebuggerMessage::Run => todo!(),
DebuggerMessage::Pause => todo!(),
}
}
}
fn run_type<'a, Message: Clone + 'a>(
label: &'a str,
val: usize,
update: impl Fn(usize) -> Message + 'a,
run: Message,
) -> Element<'a, Message> {
row![
widget::container(text(label)).padding(2.),
widget::container(number_input(val).on_input(update)).padding(2.),
widget::container(button(image("./images/ic_fluent_play_24_filled.png")).on_press(run))
.padding(2.),
]
.spacing(1.)
.into()
}
pub fn labelled<'a, Message: 'a>(
label: &'a str,
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![
widget::container(text(label)).padding(2.),
widget::container(content).style(bordered_box).padding(2.),
]
.spacing(1.)
.into()
}
pub fn labelled_box<'a, Message: 'a>(label: &'a str, value: bool) -> Element<'a, Message> {
row![checkbox(value), widget::container(text(label)),]
.spacing(1.)
.into()
}

View File

@@ -1,163 +1,36 @@
//! 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::advanced::graphics::core::{keyboard, touch, window};
use iced::advanced::text::paragraph;
use iced::advanced::widget::{Tree, tree};
use iced::advanced::{
Clipboard, Layout, Shell, Text, Widget, layout, mouse, overlay, renderer, text,
};
use iced::overlay::menu::Menu;
use iced::{
Background, Border, Color, Element, Event, Point, Rectangle, Size, Theme, Vector, alignment,
};
use iced::{
Length, Padding, Pixels,
widget::{button, overlay::menu},
};
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,
pub fn header_menu<'a, T, L, Message: Clone>(
title: impl ToString,
options: L,
f: impl Fn(T) -> Message + 'a,
) -> HeaderMenu<'a, T, L, Message>
where
T: ToString + Clone,
L: Borrow<[T]> + 'a,
{
HeaderMenu::new(title, options, f)
}
pub struct HeaderMenu<'a, T, L, Message, Theme = iced::Theme, Renderer = iced::Renderer>
where
T: ToString + Clone,
L: Borrow<[T]> + 'a,
// V: Borrow<T> + 'a,
Theme: Catalog,
Renderer: text::Renderer,
{
@@ -176,35 +49,30 @@ pub struct HeaderMenu<
handle: Handle<Renderer::Font>,
class: <Theme as Catalog>::Class<'a>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
last_status: Option<Status>,
menu_height: Length,
}
impl<'a, T, L, Message, Theme, Renderer> HeaderMenu<'a, T, L, Message, Theme, Renderer>
where
T: ToString + PartialEq + Clone,
T: ToString + 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 {
pub fn new(title: impl ToString, options: L, on_select: impl Fn(T) -> Message + 'a) -> Self {
Self {
title: title.into(),
on_select: Box::new(on_click),
title: title.to_string(),
on_select: Box::new(on_select),
on_open: None,
on_close: None,
options,
placeholder: None,
// selected,
width: Length::Shrink,
padding: Padding::new(0.1),
padding: button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
@@ -212,6 +80,8 @@ where
handle: Handle::default(),
class: <Theme as Catalog>::default(),
menu_class: <Theme as Catalog>::default_menu(),
last_status: None,
menu_height: Length::Shrink,
}
}
@@ -227,6 +97,12 @@ where
self
}
/// Sets the height of the [`Menu`].
pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
self.menu_height = menu_height.into();
self
}
/// Sets the [`Padding`] of the [`PickList`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
@@ -294,14 +170,27 @@ where
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`PickList`].
#[must_use]
pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
self.class = class.into();
self
}
/// Sets the style class of the [`Menu`].
#[must_use]
pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
self.menu_class = class.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,
T: Clone + ToString + 'a,
L: Borrow<[T]>,
// V: Borrow<T>,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
@@ -322,7 +211,7 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
@@ -344,8 +233,8 @@ where
size: text_size,
line_height: self.text_line_height,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
};
@@ -353,14 +242,14 @@ where
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
paragraph.update(Text {
let _ = paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = &self.placeholder {
state.placeholder.update(Text {
let _ = state.placeholder.update(Text {
content: placeholder,
..option_text
});
@@ -398,22 +287,22 @@ where
layout::Node::new(size)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
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.
@@ -423,58 +312,30 @@ where
shell.publish(on_close.clone());
}
event::Status::Captured
shell.capture_event();
} 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
shell.capture_event();
}
}
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()
}
let next_option = if *y < 0.0 {
options.first()
} else if *y > 0.0 {
options.last()
} else {
None
};
@@ -483,19 +344,34 @@ where
shell.publish((self.on_select)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
state.keyboard_modifiers = *modifiers;
}
_ => event::Status::Ignored,
_ => {}
};
let status = {
let is_hovered = cursor.is_over(layout.bounds());
if state.is_open {
Status::Opened { is_hovered }
} else if is_hovered {
Status::Hovered
} else {
Status::Active
}
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(status);
} else if self
.last_status
.is_some_and(|last_status| last_status != status)
{
shell.request_redraw();
}
}
@@ -524,26 +400,19 @@ where
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_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);
let style = Catalog::style(
theme,
&self.class,
self.last_status.unwrap_or(Status::Active),
);
renderer.fill_quad(
renderer::Quad {
@@ -601,8 +470,8 @@ where
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,
align_x: text::Alignment::Right,
align_y: alignment::Vertical::Center,
shaping,
wrapping: text::Wrapping::default(),
},
@@ -610,13 +479,11 @@ where
bounds.x + bounds.width - self.padding.right,
bounds.center_y(),
),
style.text_color,
style.handle_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(
@@ -626,20 +493,16 @@ where
line_height: self.text_line_height,
font,
bounds: Size::new(
bounds.width - self.padding.horizontal(),
bounds.width - self.padding.x(),
f32::from(self.text_line_height.to_absolute(text_size)),
),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
align_x: text::Alignment::Default,
align_y: 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,
// },
style.text_color,
*viewport,
);
}
@@ -649,6 +512,7 @@ where
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
@@ -680,7 +544,12 @@ where
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position() + translation, bounds.height))
Some(menu.overlay(
layout.position() + translation,
*viewport,
bounds.height,
self.menu_height,
))
} else {
None
}
@@ -690,9 +559,8 @@ where
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,
T: Clone + ToString + 'a,
L: Borrow<[T]> + 'a,
// V: Borrow<T> + 'a,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
@@ -784,11 +652,14 @@ pub enum Status {
/// The [`PickList`] is being hovered.
Hovered,
/// The [`PickList`] is open.
Opened,
Opened {
/// Whether the [`PickList`] is hovered, while open.
is_hovered: bool,
},
}
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The text [`Color`] of the pick list.
pub text_color: Color,
@@ -843,7 +714,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
let active = Style {
text_color: palette.background.weak.text,
background: palette.background.weak.color.into(),
placeholder_color: palette.background.strong.color,
placeholder_color: palette.secondary.base.color,
handle_color: palette.background.weak.text,
border: Border {
radius: 2.0.into(),
@@ -854,7 +725,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
match status {
Status::Active => active,
Status::Hovered | Status::Opened => Style {
Status::Hovered | Status::Opened { .. } => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
@@ -863,82 +734,3 @@ pub fn default(theme: &Theme, status: Status) -> Style {
},
}
}
/// 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)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,12 @@
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,
Point, Renderer, Size, Subscription, Task, Theme, mouse,
widget::{
Canvas, button,
canvas::{Frame, Program},
@@ -18,13 +15,13 @@ use iced::{
window::{self, Id, Settings},
};
use nes_emu::{
NES, PPU, RenderBuffer,
header_menu::header_menu,
hex_view::{self, HexEvent, HexView},
debugger::{DebuggerMessage, DebuggerState}, header_menu::header_menu, hex_view::{HexEvent, HexView}, NES, PPU
};
use tracing::{debug, info};
use tracing_subscriber::EnvFilter;
const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "basic_init_1.nes");
// const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
extern crate nes_emu;
fn main() -> Result<(), iced::Error> {
@@ -32,16 +29,11 @@ fn main() -> Result<(), iced::Error> {
.with_env_filter(EnvFilter::from_default_env())
.init();
iced::daemon(Emulator::title, Emulator::update, Emulator::view)
iced::daemon(Emulator::new, 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()
.theme(Theme::Dark)
.title(Emulator::title)
.run()
}
enum MemoryTy {
@@ -53,6 +45,7 @@ enum WindowType {
Memory(MemoryTy, HexView),
TileMap,
TileViewer,
Debugger,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -60,6 +53,7 @@ enum HeaderButton {
OpenMemory,
OpenTileMap,
OpenTileViewer,
OpenDebugger,
Reset,
PowerCycle,
}
@@ -70,6 +64,7 @@ impl fmt::Display for HeaderButton {
Self::OpenMemory => write!(f, "Open Memory Viewer"),
Self::OpenTileMap => write!(f, "Open TileMap Viewer"),
Self::OpenTileViewer => write!(f, "Open Tile Viewer"),
Self::OpenDebugger => write!(f, "Open Debugger"),
Self::Reset => write!(f, "Reset"),
Self::PowerCycle => write!(f, "Power Cycle"),
}
@@ -79,6 +74,7 @@ impl fmt::Display for HeaderButton {
struct Emulator {
nes: NES,
windows: HashMap<Id, WindowType>,
debugger: DebuggerState,
}
#[derive(Debug, Clone)]
@@ -92,13 +88,12 @@ enum Message {
WindowClosed(Id),
Header(HeaderButton),
Hex(Id, HexEvent),
Debugger(DebuggerMessage),
}
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)
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());
@@ -106,6 +101,7 @@ impl Emulator {
Self {
nes,
windows: HashMap::from_iter([(win, WindowType::Main)]),
debugger: DebuggerState::new(),
},
task.map(Message::WindowOpened),
)
@@ -116,6 +112,7 @@ impl Emulator {
Some(WindowType::Memory(_, _)) => "NES MemoryView".into(),
Some(WindowType::TileMap) => "NES TileMap".into(),
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
Some(WindowType::Debugger) => "NES Debugger".into(),
None => todo!(),
}
}
@@ -129,38 +126,42 @@ impl Emulator {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Tick(count) => {
for _ in 0..count {
self.nes.run_one_clock_cycle();
}
}
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::WindowOpened(_id) => {
// Window
}
Message::WindowClosed(id) => {
if let Some(WindowType::Main) = self.windows.remove(&id) {
return iced::exit();
}
}
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()));
}
return self.open(WindowType::Memory(MemoryTy::Cpu, HexView::new()));
}
Message::Header(HeaderButton::OpenTileMap) => {
return self.open(WindowType::TileMap);
}
return self.open(WindowType::TileMap);
}
Message::Header(HeaderButton::OpenTileViewer) => {
return self.open(WindowType::TileViewer);
}
return self.open(WindowType::TileViewer);
}
Message::Header(HeaderButton::OpenDebugger) => {
return self.open(WindowType::Debugger);
}
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));
}
}
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),
}
// self.image.0.clone_from(self.nes.image());
Task::none()
@@ -197,7 +198,11 @@ impl Emulator {
Some(WindowType::TileViewer) => {
container(Canvas::new(DbgImage::NameTable(self.nes.ppu()))).width(Fill).height(Fill).into()
}
_ => todo!(),
Some(WindowType::Debugger) => {
container(self.debugger.view(&self.nes).map(Message::Debugger)).width(Fill).height(Fill).into()
}
None => panic!("Window not found"),
// _ => todo!(),
}
}
@@ -235,6 +240,7 @@ impl Emulator {
header_menu(
"Debugging",
[
HeaderButton::OpenDebugger,
HeaderButton::OpenMemory,
HeaderButton::OpenTileMap,
HeaderButton::OpenTileViewer,
@@ -252,13 +258,13 @@ impl Program<Message> for Emulator {
fn draw(
&self,
state: &Self::State,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: iced::Rectangle,
_theme: &Theme,
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
const SIZE: f32 = 2.;
// const SIZE: f32 = 2.;
let mut frame = Frame::new(
renderer,
iced::Size {
@@ -291,13 +297,13 @@ impl Program<Message> for DbgImage<'_> {
fn draw(
&self,
state: &Self::State,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: iced::Rectangle,
_theme: &Theme,
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
const SIZE: f32 = 2.;
// 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.);

View File

@@ -1,5 +1,4 @@
use iced::{Point, Size, widget::canvas::Frame};
use iced_graphics::geometry::{Fill, Renderer};
use iced::{advanced::graphics::geometry::Renderer, widget::canvas::{Fill, Frame}, Point, Size};
use crate::{hex_view::Memory, mem::{MemoryMap, Segment}};
@@ -81,7 +80,7 @@ pub struct Background {
cur_shift_low: u8,
}
struct Mask {
pub struct Mask {
grayscale: bool,
background_on_left_edge: bool,
sprites_on_left_edge: bool,
@@ -97,12 +96,12 @@ pub struct PPU {
frame_count: usize,
nmi_enabled: bool,
nmi_waiting: bool,
even: bool,
scanline: usize,
pixel: usize,
pub even: bool,
pub scanline: usize,
pub pixel: usize,
mask: Mask,
vblank: bool,
pub mask: Mask,
pub vblank: bool,
pub(crate) memory: MemoryMap<PPUMMRegisters>,
background: Background,
@@ -191,6 +190,7 @@ impl PPU {
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
self.vblank = false;
self.background.w = false;
println!("Reading status: {:02X}", tmp);
tmp
}
3 => panic!("oamaddr is write-only"),
@@ -357,6 +357,9 @@ impl PPU {
return false;
}
pub fn peek_nmi(&self) -> bool {
self.nmi_waiting
}
pub fn nmi_waiting(&mut self) -> bool {
if self.nmi_waiting {
self.nmi_waiting = false;
@@ -365,6 +368,9 @@ impl PPU {
return false;
}
}
pub fn peek_irq(&self) -> bool {
self.nmi_waiting
}
pub fn irq_waiting(&mut self) -> bool {
false
}

View File

@@ -0,0 +1,43 @@
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM
.inesmir 0 ; background mirroring, horizontal
.org $8000
RESET:
sei ; Ignore IRQs while starting up
cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway)
ldx #$40
stx $4017 ; Disable APU frame IRQ
ldx #$ff
txs ; Set stack pointer to 0x1ff
inx ; Set x to zero
stx $2000 ; Disable NMI (by writing zero)
stx $2001 ; Disable rendering
stx $4010 ; Disable DMC IRQs
bit $2002 ; Clear vblank flag by reading ppu status
; VBLANKWAIT1:
; bit $2002
; bpl VBLANKWAIT1
; VBLANKWAIT2:
; bit $2002
; bpl VBLANKWAIT2
hlt
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"

View File

@@ -0,0 +1,43 @@
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM
.inesmir 0 ; background mirroring, horizontal
.org $8000
RESET:
sei ; Ignore IRQs while starting up
cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway)
ldx #$40
stx $4017 ; Disable APU frame IRQ
ldx #$ff
txs ; Set stack pointer to 0x1ff
inx ; Set x to zero
stx $2000 ; Disable NMI (by writing zero)
stx $2001 ; Disable rendering
stx $4010 ; Disable DMC IRQs
bit $2002 ; Clear vblank flag by reading ppu status
VBLANKWAIT1:
bit $2002
bpl VBLANKWAIT1
; VBLANKWAIT2:
; bit $2002
; bpl VBLANKWAIT2
hlt
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"

View File

@@ -2,12 +2,15 @@ use crate::{NES, hex_view::Memory};
macro_rules! rom_test {
($name:ident, $rom:literal, |$nes:ident| $eval:expr) => {
rom_test!($name, $rom, timeout = 10000000, |$nes| $eval);
};
($name:ident, $rom:literal, timeout = $timeout:expr, |$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);
$nes.reset_and_run_with_timeout($timeout);
println!("Final: {:?}", $nes);
$eval
}
@@ -39,3 +42,24 @@ rom_test!(read_write, "read_write.nes", |nes| {
assert_eq!(nes.cpu_mem().peek(0x0001).unwrap(), 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0002).unwrap(), 0xAA);
});
rom_test!(basic_init_0, "basic_init_0.nes", |nes| {
assert_eq!(nes.last_instruction, "0x8017 HLT :2 []");
assert_eq!(nes.cycle, 41);
assert_eq!(nes.cpu.pc, 0x8018);
assert_eq!(nes.cpu.sp, 0xFF);
assert_eq!(nes.cpu.a, 0x00);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});
rom_test!(basic_init_1, "basic_init_1.nes", |nes| {
assert_eq!(nes.last_instruction, "0x801C HLT :2 []");
assert_eq!(nes.cycle, 27403);
assert_eq!(nes.ppu.pixel, 30);
assert_eq!(nes.cpu.pc, 0x801D);
assert_eq!(nes.cpu.sp, 0xFF);
assert_eq!(nes.cpu.a, 0x00);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});

View File

@@ -1,5 +1,3 @@
; FINAL = ""
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM

16888
test-emu/AccuracyCoin.asm Normal file

File diff suppressed because it is too large Load Diff

BIN
test-emu/Sprites.pcx Normal file

Binary file not shown.

BIN
test-emu/Tiles.pcx Normal file

Binary file not shown.

26
test-rom/basic-cpu.asm Normal file
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 ;;;;
; .incchr "Sprites.pcx"
; .incchr "Tiles.pcx"