Files
nes-emu/src/debugger.rs
Matthew Pomes 148ab2004d
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Has been cancelled
Minor refactors and bug fixes
- Bit instruction now sets Z flag correctly
- DMA is no longer handled by cpu.rs
2026-01-26 01:21:37 -06:00

668 lines
22 KiB
Rust

use std::rc::Rc;
use iced::{
Element,
Length::{self, Fill},
Point, Renderer, Size,
advanced::{
Widget,
layout::Node,
widget::{
Tree,
tree::{State, Tag},
},
},
mouse,
widget::{
self, Canvas, Text, button,
canvas::{Frame, Program},
checkbox, column,
container::bordered_box,
hex_input, image, number_input, row,
rule::horizontal,
scrollable, text,
},
};
use crate::{Break, CycleResult, NES, PPU, mem::Mapped};
#[derive(Debug, Clone)]
pub struct DebuggerState {
ppu_cycles: usize,
cpu_cycles: usize,
instructions: usize,
scan_lines: usize,
to_scan_line: usize,
frames: usize,
breakpoint: 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,
SetBreakpoint(usize),
RunBreakpoint,
}
pub fn hex16<'a, Theme, Renderer>(val: u16) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:04X}"))
}
pub fn hex8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:02X}"))
}
pub fn bin8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:08b}"))
}
pub fn bin32<'a, Theme, Renderer>(val: u32) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:032b}"))
}
impl DebuggerState {
pub fn new() -> Self {
Self {
ppu_cycles: 1,
cpu_cycles: 1,
instructions: 1,
scan_lines: 1,
to_scan_line: 1,
frames: 1,
breakpoint: 0xEA5A,
// 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.cpu_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", nes.apu().peek_irq()),
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)),
labelled("Frame", text(nes.ppu().frame_count)),
labelled("V:", hex16(nes.ppu().background.v)),
labelled("T:", hex16(nes.ppu().background.t)),
labelled("X:", hex8(nes.ppu().background.x)),
text(""),
labelled("NT:", hex8(nes.ppu().background.cur_nametable)),
labelled2(
"AT:",
hex8(nes.ppu().background.next_attr),
hex16(
0x23C0
| (nes.ppu().background.v & 0x0C00)
| ((nes.ppu().background.v >> 4) & 0x38)
| ((nes.ppu().background.v >> 2) & 0x07)
)
),
labelled("AT:", hex8(nes.ppu().background.cur_attr)),
labelled("HI:", bin32(nes.ppu().background.cur_shift_high)),
labelled("LO:", bin32(nes.ppu().background.cur_shift_low)),
],
column![
labelled_box("Sprite 0 Hit", false),
labelled_box("Sprite 0 Overflow", false),
labelled_box("Vertical Blank", nes.ppu().vblank),
labelled_box("Write Toggle", nes.ppu().background.w),
text(""),
labelled_box("Large Sprites", false),
labelled_box("Vertical Write", nes.ppu().background.vram_column),
labelled_box("NMI on VBlank", nes.ppu().nmi_on_vblank()),
labelled_box("BG at $1000", nes.ppu().background.second_pattern),
labelled_box("Sprites at $1000", false),
],
column![
labelled_box("Even frame", nes.ppu().even),
labelled_box("BG Enabled", nes.ppu().mask.enable_background),
labelled_box("Sprites Enabled", nes.ppu().mask.enable_sprites),
labelled_box("BG Mask", nes.ppu().mask.background_on_left_edge),
labelled_box("Sprites Mask", nes.ppu().mask.sprites_on_left_edge),
labelled_box("Grayscale", nes.ppu().mask.grayscale),
labelled_box("Intensify Red", nes.ppu().mask.em_red),
labelled_box("Intensify Green", nes.ppu().mask.em_green),
labelled_box("Intensify Blue", nes.ppu().mask.em_blue),
],
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
),
run_type_hex(
"To Address:",
self.breakpoint,
DebuggerMessage::SetBreakpoint,
DebuggerMessage::RunBreakpoint
),
],
]
.spacing(5.),
horizontal(2),
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 {
if nes.run_one_clock_cycle(&Break::default()).dbg_int || nes.halted() {
break;
}
}
}
fn run_until(
nes: &mut NES,
br: &Break,
mut f: impl FnMut(CycleResult, &NES) -> bool,
// mut count: usize,
) {
// Always run at least 1 cycle
let mut res = nes.run_one_clock_cycle(&Break::default());
while !nes.halted() && !res.dbg_int && !f(res, nes) {
// if res.dbg_int || f(res, nes) {
// count -= 1;
// if count <= 0 {
// break;
// }
// }
// if nes.halted() {
// break;
// }
res = nes.run_one_clock_cycle(br);
}
}
fn run_until_n(
nes: &mut NES,
br: &Break,
mut f: impl FnMut(CycleResult, &NES) -> bool,
mut count: usize,
) {
while count > 0 {
Self::run_until(nes, br, &mut f);
count -= 1;
}
}
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.min(261), // Max scanline is 261
DebuggerMessage::SetFrames(n) => self.frames = n,
DebuggerMessage::SetBreakpoint(n) => self.breakpoint = 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_n(
// nes,
// &Break {
// cpu_exec: true,
// ..Break::default()
// },
// |_, _| false,
// self.instructions,
// ),
DebuggerMessage::RunInstructions => Self::run_until_n(
nes,
&Break {
..Break::default()
},
|res, _| res.cpu_exec,
self.instructions,
),
DebuggerMessage::RunScanLines => Self::run_n_clock_cycles(nes, self.scan_lines * 341),
DebuggerMessage::RunToScanLine => Self::run_until(
nes,
&Break {
ppu_scanline: true,
..Break::default()
},
|_, n| n.ppu.scanline == self.to_scan_line,
),
DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262),
DebuggerMessage::RunBreakpoint => Self::run_until(
nes,
&Break {
break_points: vec![self.breakpoint as u16],
..Break::default()
},
|_, nes| nes.cpu.pc as usize == self.breakpoint,
),
DebuggerMessage::Run => {
Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
}
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).on_submit(run.clone())).padding(2.),
widget::container(button(image("./images/ic_fluent_play_24_filled.png")).on_press(run))
.padding(2.),
]
.spacing(1.)
.into()
}
fn run_type_hex<'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(hex_input(val).on_input(update).on_submit(run.clone())).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 labelled2<'a, Message: 'a>(
label: &'a str,
content: impl Into<Element<'a, Message>>,
content2: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![
widget::container(text(label)).padding(2.),
widget::container(content).style(bordered_box).padding(2.),
widget::container(content2).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()
}
#[derive(Clone, Copy)]
pub enum DbgImage<'a> {
NameTable(&'a Mapped, &'a PPU),
PatternTable(&'a Mapped, &'a PPU),
Palette(&'a Mapped, &'a PPU),
}
impl<Message, T> Program<Message, T> for DbgImage<'_> {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &T,
_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.);
// println!("Position: {:?}", cursor.position());
match self {
DbgImage::NameTable(mem, ppu) => ppu.render_name_table(mem, &mut name_table_frame),
DbgImage::PatternTable(mem, ppu) => {
ppu.render_pattern_tables(mem, &mut name_table_frame)
}
DbgImage::Palette(_, ppu) => ppu.render_palette(&mut name_table_frame),
};
vec![name_table_frame.into_geometry()]
}
}
impl DbgImage<'_> {
fn width(&self) -> Length {
match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(40. * 2.),
}
}
fn height(&self) -> Length {
match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(80. * 2.),
}
}
fn help(&self, cursor: Point) -> Option<String> {
match self {
DbgImage::NameTable(mem, ppu) => ppu.name_cursor_info(mem, cursor),
DbgImage::PatternTable(_, ppu) => ppu.pattern_cursor_info(cursor),
DbgImage::Palette(_, ppu) => ppu.palette_cursor_info(cursor),
}
}
}
struct DbgImageSetup<'a, M, T: text::Catalog> {
dbg: DbgImage<'a>,
image: Canvas<DbgImage<'a>, M, T>,
text: Text<'a, T>,
padding: f32,
drawn: bool,
// image: DbgImage<'a>,
// text: te,
}
// pub trait Container<Message, Theme, Renderer> {
// // Not ideal
// fn children(&self) -> &[&dyn Widget<Message, Theme, Renderer>];
// }
impl<'s, Message, Theme> Widget<Message, Theme, Renderer> for DbgImageSetup<'s, Message, Theme>
where
Theme: text::Catalog + 's,
{
fn size(&self) -> Size<iced::Length> {
// self.
Size::new(Fill, Fill)
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &iced::advanced::layout::Limits,
) -> Node {
let img_node = self.image.layout(&mut tree.children[0], renderer, limits);
let txt_node = Widget::<Message, Theme, _>::layout(
&mut self.text,
&mut tree.children[1],
renderer,
&limits.shrink(Size::new(
img_node.size().width + self.padding * 2.,
self.padding * 2.,
)),
)
.move_to(Point::new(
img_node.size().width + self.padding,
self.padding,
));
Node::with_children(limits.max(), vec![img_node, txt_node])
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced::advanced::renderer::Style,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
self.image.draw(
&tree.children[0],
renderer,
theme,
style,
layout.child(0),
cursor,
viewport,
);
Widget::<Message, Theme, _>::draw(
&self.text,
&tree.children[1],
renderer,
theme,
style,
layout.child(1),
cursor,
viewport,
)
}
fn tag(&self) -> Tag {
Tag::of::<Rc<String>>()
}
fn state(&self) -> State {
State::new(Rc::new(String::new()))
}
fn children(&self) -> Vec<Tree> {
vec![
Tree::new(&self.image as &dyn Widget<Message, Theme, Renderer>),
Tree::new(&self.text as &dyn Widget<Message, Theme, Renderer>),
]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&[
&self.image as &dyn Widget<Message, Theme, Renderer>,
&self.text as &dyn Widget<Message, Theme, Renderer>,
]);
}
fn operate(
&mut self,
tree: &mut Tree,
layout: iced::advanced::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced::advanced::widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |op| {
self.image
.operate(&mut tree.children[0], layout.child(0), renderer, op);
Widget::<Message, Theme, _>::operate(
&mut self.text,
&mut tree.children[1],
layout.child(1),
renderer,
op,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &iced::Event,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn iced::advanced::Clipboard,
shell: &mut iced::advanced::Shell<'_, Message>,
viewport: &iced::Rectangle,
) {
self.image.update(
&mut tree.children[0],
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if matches!(event, iced::Event::Mouse(mouse::Event::CursorMoved { .. })) || !self.drawn {
if let Some(help) = cursor
.position_in(layout.child(0).bounds())
.and_then(|pos| self.dbg.help(Point::new(pos.x / 2., pos.y / 2.)))
{
self.text = text(help);
shell.invalidate_layout();
shell.request_redraw();
}
self.drawn = true;
}
Widget::<Message, Theme, _>::update(
&mut self.text,
&mut tree.children[1],
event,
layout.child(1),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
_tree: &iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'_>,
_cursor: iced::advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> iced::advanced::mouse::Interaction {
iced::advanced::mouse::Interaction::default()
}
fn overlay<'a>(
&'a mut self,
_tree: &'a mut iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'a>,
_renderer: &Renderer,
_viewport: &iced::Rectangle,
_translation: iced::Vector,
) -> Option<iced::advanced::overlay::Element<'a, Message, Theme, Renderer>> {
None
}
}
pub fn dbg_image<'a, Message: 'a>(img: DbgImage<'a>) -> Element<'a, Message> {
Element::new(DbgImageSetup {
dbg: img,
image: Canvas::new(img).width(img.width()).height(img.height()),
text: text(""),
padding: 10.,
drawn: false,
})
}