From cd3de5e361bbefb7c810c1b68d1048fdfe25a84c Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Mon, 19 Jan 2026 01:36:58 -0600 Subject: [PATCH] Major work --- src/apu.rs | 44 +- src/debug.rs | 4 + src/debugger.rs | 378 ++++++++++++-- src/lib.rs | 187 +++++-- src/main.rs | 89 ++-- src/mem.rs | 107 +++- src/ppu.rs | 483 +++++++++++++----- src/test_roms/asm6f | Bin 66952 -> 0 bytes src/test_roms/basic-cpu-nop.s | 13 + src/test_roms/basic-cpu.asm | 26 - src/test_roms/basic-cpu.s | 13 + .../{basic_init_0.asm => basic_init_0.s} | 30 +- src/test_roms/basic_init_1.asm | 43 -- src/test_roms/basic_init_1.s | 26 + .../{basic_init_2.asm => basic_init_2.s} | 30 +- .../{basic_init_3.asm => basic_init_3.s} | 30 +- src/test_roms/common/shell.inc | 14 +- src/test_roms/common/testing.s | 128 ++--- src/test_roms/{even_odd.asm => even_odd.s} | 30 +- src/test_roms/int_nmi_exit_timing.s | 38 ++ src/test_roms/int_nmi_timing.s | 31 ++ src/test_roms/int_nmi_while_nmi.s | 39 ++ src/test_roms/interrupts.rs | 27 + src/test_roms/mod.rs | 66 ++- src/test_roms/nes.cfg | 2 +- src/test_roms/pat.bin | Bin 0 -> 64 bytes src/test_roms/ppu.rs | 90 ++++ src/test_roms/ppu_fill.s | 44 ++ src/test_roms/ppu_fill_name_table.s | 137 +++++ src/test_roms/ppu_fill_palette_1.s | 67 +++ src/test_roms/ppu_fill_red.s | 46 ++ src/test_roms/ppu_vertical_write.s | 48 ++ src/test_roms/read_write.asm | 29 -- src/test_roms/read_write.s | 17 + 34 files changed, 1750 insertions(+), 606 deletions(-) delete mode 100755 src/test_roms/asm6f create mode 100644 src/test_roms/basic-cpu-nop.s delete mode 100644 src/test_roms/basic-cpu.asm create mode 100644 src/test_roms/basic-cpu.s rename src/test_roms/{basic_init_0.asm => basic_init_0.s} (59%) delete mode 100644 src/test_roms/basic_init_1.asm create mode 100644 src/test_roms/basic_init_1.s rename src/test_roms/{basic_init_2.asm => basic_init_2.s} (59%) rename src/test_roms/{basic_init_3.asm => basic_init_3.s} (61%) rename src/test_roms/{even_odd.asm => even_odd.s} (59%) create mode 100644 src/test_roms/int_nmi_exit_timing.s create mode 100644 src/test_roms/int_nmi_timing.s create mode 100644 src/test_roms/int_nmi_while_nmi.s create mode 100644 src/test_roms/interrupts.rs create mode 100644 src/test_roms/pat.bin create mode 100644 src/test_roms/ppu.rs create mode 100644 src/test_roms/ppu_fill.s create mode 100644 src/test_roms/ppu_fill_name_table.s create mode 100644 src/test_roms/ppu_fill_palette_1.s create mode 100644 src/test_roms/ppu_fill_red.s create mode 100644 src/test_roms/ppu_vertical_write.s delete mode 100644 src/test_roms/read_write.asm create mode 100644 src/test_roms/read_write.s diff --git a/src/apu.rs b/src/apu.rs index d208f93..aec71d3 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -46,13 +46,20 @@ struct DeltaChannel { enabled: bool, } +struct FrameCounter { + count: usize, + mode_5_step: bool, + interrupt_enabled: bool, + irq: bool, +} + pub struct APU { pulse_1: PulseChannel, pulse_2: PulseChannel, triangle: TriangleChannel, noise: NoiseChannel, dmc: DeltaChannel, - frame_counter: u8, + frame_counter: FrameCounter, } impl std::fmt::Debug for APU { @@ -74,7 +81,12 @@ impl APU { triangle: TriangleChannel { enabled: false }, noise: NoiseChannel { enabled: false }, dmc: DeltaChannel { enabled: false }, - frame_counter: 0, + frame_counter: FrameCounter { + mode_5_step: false, + interrupt_enabled: true, + count: 0, + irq: false, + }, } } pub fn read_reg(&mut self, offset: u16) -> u8 { @@ -98,11 +110,28 @@ impl APU { 0x11 => { // TODO: load dmc counter with (val & 7F) } - _ => panic!("No register at {:X}", offset), + _ => (), + // _ => panic!("No register at {:X}", offset), } } - pub fn run_one_clock_cycle(&mut self) -> bool { + pub fn run_one_clock_cycle(&mut self, ppu_cycle: usize) -> bool { + if ppu_cycle % 6 == 1 { + // APU Frame Counter clock cycle + if !self.frame_counter.mode_5_step + && self.frame_counter.interrupt_enabled + && self.frame_counter.count == 14914 + { + self.frame_counter.irq = true; + } else if !self.frame_counter.mode_5_step + && self.frame_counter.interrupt_enabled + && self.frame_counter.count == 14915 + { + self.frame_counter.irq = true; + self.frame_counter.count = 0; + } + self.frame_counter.count += 1; + } false } pub fn peek_nmi(&self) -> bool { @@ -112,10 +141,11 @@ impl APU { false } pub fn peek_irq(&self) -> bool { - false + self.frame_counter.irq } pub fn irq_waiting(&mut self) -> bool { - // TODO: implement logic - false + let res = self.frame_counter.irq; + self.frame_counter.irq = false; + res } } diff --git a/src/debug.rs b/src/debug.rs index 0968144..0f413c5 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -42,6 +42,10 @@ impl DebugLog { pub fn history(&self) -> &[String] { &self.history[self.history.len().saturating_sub(100)..] } + + pub fn pop(&mut self) -> Option { + self.history.pop() + } } impl std::fmt::Write for DebugLog { diff --git a/src/debugger.rs b/src/debugger.rs index faf6144..a703702 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,13 +1,16 @@ +use std::rc::Rc; + use iced::{ - Element, - Length::Fill, - widget::{ - self, button, checkbox, column, container::bordered_box, image, number_input, row, - scrollable, text, - }, + advanced::{ + layout::Node, widget::{ + tree::{State, Tag}, Tree + }, Widget + }, mouse, widget::{ + self, button, canvas::{Frame, Program}, checkbox, column, container::bordered_box, image, number_input, row, rule::horizontal, scrollable, text, Canvas, Text + }, window::Event, Element, Length::{self, Fill}, Point, Renderer, Size, Theme }; -use crate::{CycleResult, NES}; +use crate::{CycleResult, NES, PPU}; #[derive(Debug, Clone)] pub struct DebuggerState { @@ -37,6 +40,35 @@ pub enum DebuggerMessage { RunFrames, } +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 { @@ -85,38 +117,56 @@ impl DebuggerState { text("IRQs:"), labelled_box("NMI", nes.peek_nmi()), labelled_box("Cart", false), - labelled_box("Frame Counter", 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("Cycle", text(nes.ppu().pixel)), + labelled("Scanline", text(nes.ppu().scanline)), + labelled("PPU Cycle", text(nes.ppu().cycle)), + 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", false), - labelled_box("", 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", false), - labelled_box("NMI on VBlank", false), - labelled_box("BG at $1000", 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", false), - labelled_box("Sprites Enabled", false), - labelled_box("BG Mask", false), - labelled_box("Sprites Mask", false), - labelled_box("Grayscale", false), - labelled_box("Intensify Red", false), - labelled_box("Intensify Green", false), - labelled_box("Intensify Blue", false), + 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( @@ -158,6 +208,7 @@ impl DebuggerState { ], ] .spacing(5.), + horizontal(2), scrollable( column( nes.debug_log() @@ -178,15 +229,15 @@ impl DebuggerState { fn run_n_clock_cycles(nes: &mut NES, n: usize) { for _ in 0..n { - nes.run_one_clock_cycle(); - if nes.halted { + if nes.run_one_clock_cycle().dbg_int || nes.halted { break; } } } fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool, mut count: usize) { loop { - if f(nes.run_one_clock_cycle(), nes) { + let res = nes.run_one_clock_cycle(); + if res.dbg_int || f(res, nes) { count -= 1; if count <= 0 { break; @@ -250,8 +301,277 @@ pub fn labelled<'a, Message: 'a>( .into() } +pub fn labelled2<'a, Message: 'a>( + label: &'a str, + content: impl Into>, + content2: impl Into>, +) -> 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 PPU), + PatternTable(&'a PPU), + Palette(&'a PPU), +} + +impl Program for DbgImage<'_> { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + _theme: &T, + _bounds: iced::Rectangle, + _cursor: mouse::Cursor, + ) -> Vec> { + // 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(ppu) => ppu.render_name_table(&mut name_table_frame), + DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame), + DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame), + }; + vec![name_table_frame.into_geometry()] + } +} + +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 { + match self { + DbgImage::NameTable(ppu) => ppu.name_cursor_info(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, M, T>, + text: Text<'a, T>, + padding: f32, + drawn: bool, + // image: DbgImage<'a>, + // text: te, +} + +// pub trait Container { +// // Not ideal +// fn children(&self) -> &[&dyn Widget]; +// } + +impl<'s, Message, Theme> Widget for DbgImageSetup<'s, Message, Theme> +where + Theme: text::Catalog + 's, +{ + fn size(&self) -> Size { + // 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::::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::::draw( + &self.text, + &tree.children[1], + renderer, + theme, + style, + layout.child(1), + cursor, + viewport, + ) + } + + fn tag(&self) -> Tag { + Tag::of::>() + } + + fn state(&self) -> State { + State::new(Rc::new(String::new())) + } + + fn children(&self) -> Vec { + vec![ + Tree::new(&self.image as &dyn Widget), + Tree::new(&self.text as &dyn Widget), + ] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[ + &self.image as &dyn Widget, + &self.text as &dyn Widget, + ]); + } + + 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::::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::::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> { + 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, + }) +} diff --git a/src/lib.rs b/src/lib.rs index 713d498..86019bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,9 @@ pub mod header_menu; pub mod hex_view; mod mem; mod ppu; +pub mod resize_watcher; #[cfg(test)] mod test_roms; -pub mod resize_watcher; pub use ppu::{Color, PPU, RenderBuffer}; @@ -22,7 +22,7 @@ use crate::{ controllers::Controllers, debug::DebugLog, hex_view::Memory, - mem::{MemoryMap, Segment}, + mem::{Mapper, MemoryMap, Segment}, }; #[derive(Error, Debug)] @@ -39,6 +39,7 @@ pub struct NES { cpu: Cpu, dma: DmaState, memory: MemoryMap, + mapper: Mapper, ppu: PPU, apu: APU, controller: Controllers, @@ -159,8 +160,13 @@ pub struct Cpu { pub sp: u8, pub status: CpuStatus, pub clock_state: ClockState, + + pub nmi_line_state: bool, + pub nmi_pending: bool, + pub irq_pending: bool, } +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum ClockState { ReadInstruction, ReadOperands { @@ -249,6 +255,9 @@ impl Cpu { count: 0, addr: 0, }, + nmi_pending: false, + irq_pending: false, + nmi_line_state: false, } } } @@ -287,16 +296,14 @@ impl NES { info!("PRG: {prg_rom_size}"); info!("CHR: {chr_rom_size}"); info!("FLAGS: {mapper_flags:b}"); - if mapper_flags & 0b11111110 != 0 { - todo!("Support other mapper flags"); - } Ok(Self::from_rom( &raw[16..][..16384 * prg_rom_size as usize], &raw[16 + 16384 * prg_rom_size as usize..][..8192 * chr_rom_size as usize], + Mapper::from_flags(mapper_flags), )) } - fn from_rom(prg_rom: &[u8], chr_rom: &[u8]) -> Self { + fn from_rom(prg_rom: &[u8], chr_rom: &[u8], mapper: Mapper) -> Self { let mut segments = vec![ Segment::ram("Internal RAM", 0x0000, 0x0800), Segment::mirror("Mirror of iRAM", 0x0800, 0x1800, 0), @@ -306,7 +313,8 @@ impl NES { Segment::mirror("Mirror of APU & IO", 0x4018, 0x0008, 2), ]; // let mut cur = 0x4020; - segments.push(Segment::rom("PROG ROM", 0x8000, prg_rom)); + assert!(prg_rom.len() <= 0x8000, "Mappers for larger sizes not supported"); + segments.push(Segment::rom("PROG ROM", 0x8000 + (0x8000 - prg_rom.len() as u16), prg_rom)); Self { cycle: 7, dbg_int: false, @@ -315,8 +323,9 @@ impl NES { clock_count: 0, memory: MemoryMap::new(segments), + mapper, cpu: Cpu::init(), - ppu: PPU::with_chr_rom(chr_rom), + ppu: PPU::with_chr_rom(chr_rom, mapper), apu: APU::init(), controller: Controllers::init(), dma: DmaState::Passive, @@ -430,9 +439,9 @@ impl NES { } else if !held && hold_time != 0 { ExecState::Hold(hold_time - 1) } else { - debug!("Running 0x{:04X} {} :{:X} {:X?}", self.cpu.pc - (1 + params.len() as u16), $val, ins, params); // debug!("Running 0x{:04X} {} :{:X} {:X?}", self.cpu.pc - (1 + params.len() as u16), $val, ins, params); - self.last_instruction = format!("0x{:04X} {} :{:X} {:X?}", self.cpu.pc - (1 + params.len() as u16), $val, ins, params); + // debug!("Running 0x{:04X} {} :{:X} {:X?}", self.cpu.pc - (1 + params.len() as u16), $val, ins, params); + self.last_instruction = format!("0x{:04X} {} :{:X} {:X?}", addr, $val, ins, params); $( let $name = params[0]; #[allow(unused_assignments)] @@ -462,6 +471,13 @@ impl NES { log!("{addr:04X}: HLT"); self.halt() }), + 0x12 => inst!("DBG", 0, || { + log!("{addr:04X}: DBG"); + self.halt() + }), + 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD3 | 0xF2 => { + panic!("Game crash by executing {ins:02X} at {addr:04X}") + } 0x38 => inst!("SEC", 1, || { log!("{addr:04X}: SEC"); self.cpu.status.set_carry(true) @@ -490,7 +506,8 @@ impl NES { log!("{addr:04X}: CLV"); self.cpu.status.set_overflow(false) }), - 0x00 => inst!("BRK", 7, |_ignored| { + 0x00 => inst!("BRK", 5, |_ignored| { + // TODO: this should probably just set `self.cpu.irq_pending` ... log!("{addr:04X}: BRK #${_ignored:02X}"); self.push_16(self.cpu.pc); self.push(self.cpu.status.0 | 0b00110000); @@ -1432,7 +1449,7 @@ impl NES { self.cpu.status.set_negative(self.cpu.a & 0x80 == 0x80); log!("{addr:04X}: ORA ${:02X},x | {:02X}", off, self.cpu.a); }), - 0x0D => inst!("ORA abs", 2, |low, high| { + 0x0D => inst!("ORA abs", 1, |low, high| { self.cpu.a |= self.read_abs(low, high); self.cpu.status.set_zero(self.cpu.a == 0); self.cpu.status.set_negative(self.cpu.a & 0x80 == 0x80); @@ -1443,7 +1460,7 @@ impl NES { self.cpu.a ); }), - 0x1D => inst!("ORA abs,x", 2, |low, high| { + 0x1D => inst!("ORA abs,x", 1, |low, high| { // TODO: page crossing self.cpu.a |= self.read_abs_x(low, high); self.cpu.status.set_zero(self.cpu.a == 0); self.cpu.status.set_negative(self.cpu.a & 0x80 == 0x80); @@ -1478,7 +1495,7 @@ impl NES { self.cpu.a ); }), - 0x11 => inst!("ORA (ind),y", 4, |off| { + 0x11 => inst!("ORA (ind),y", 3, |off| { let low = self.read_abs(off, 0); let high = self.read_abs(off.wrapping_add(1), 0); self.cpu.a |= self.read(u16::from_le_bytes([low, high]) + self.cpu.y as u16); @@ -1657,12 +1674,7 @@ impl NES { self.cpu.status.set_zero(val == 0); self.cpu.status.set_overflow(val & 0x40 == 0x40); self.cpu.status.set_negative(val & 0x80 == 0x80); - log!( - "{addr:04X}: BIT ${:02X}{:02X} | {:02X}", - high, - low, - val - ); + log!("{addr:04X}: BIT ${:02X}{:02X} | {:02X}", high, low, val); }), // Shifts @@ -1808,12 +1820,7 @@ impl NES { self.cpu.status.set_zero(val == 0); self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs(low, high, val); - log!( - "{addr:04X}: ROR ${:02X}{:02X} | {:02X}", - high, - low, - val - ); + log!("{addr:04X}: ROR ${:02X}{:02X} | {:02X}", high, low, val); }), 0x7E => inst!("ROR abs,x", 4, |low, high| { let old_carry = if self.cpu.status.carry() { 0x80 } else { 0x00 }; @@ -1823,12 +1830,7 @@ impl NES { self.cpu.status.set_zero(val == 0); self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs_x(low, high, val); - log!( - "{addr:04X}: ROR ${:02X}{:02X},x | {:02X}", - high, - low, - val - ); + log!("{addr:04X}: ROR ${:02X}{:02X},x | {:02X}", high, low, val); }), 0x2A => inst!("ROL A", 1, || { let old_carry = if self.cpu.status.carry() { 0x01 } else { 0x00 }; @@ -1881,12 +1883,7 @@ impl NES { self.cpu.status.set_zero(val == 0); self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs_x(low, high, val); - log!( - "{addr:04X}: ROL ${:02X}{:02X},x | {:02X}", - high, - low, - val - ); + log!("{addr:04X}: ROL ${:02X}{:02X},x | {:02X}", high, low, val); }), 0xEA => inst!("NOP", 1, || { @@ -1912,8 +1909,9 @@ impl NES { ClockState::HoldNmi { cycles } => { if cycles == 0 { self.push_16(self.cpu.pc); - self.push(self.cpu.status.0); + self.push(self.cpu.status.0 | 0x20); self.cpu.pc = u16::from_le_bytes([self.read(0xFFFA), self.read(0xFFFB)]); + writeln!(self.debug_log, "Starting NMI Handler").unwrap(); ClockState::ReadInstruction } else { ClockState::HoldNmi { cycles: cycles - 1 } @@ -1921,20 +1919,35 @@ impl NES { } ClockState::HoldIrq { cycles } => { if cycles == 0 { - todo!("Run NMI"); + // todo!("Run IRQ"); + self.push_16(self.cpu.pc); + self.push(self.cpu.status.0 | 0b00110000); + self.cpu.status.set_interrupt_disable(true); + self.cpu.pc = u16::from_le_bytes([self.read(0xFFFE), self.read(0xFFFF)]); + writeln!(self.debug_log, "Starting IRQ handler").unwrap(); + ClockState::ReadInstruction } else { ClockState::HoldIrq { cycles: cycles - 1 } } } ClockState::ReadInstruction => { - if self.ppu.nmi_waiting() || self.apu.nmi_waiting() { - ClockState::HoldNmi { cycles: 6 } - } else if self.ppu.irq_waiting() || self.apu.irq_waiting() { - ClockState::HoldIrq { cycles: 6 } - } else { + // if self.cpu.nmi_pending { + // self.cpu.nmi_pending = false; + // writeln!(self.debug_log, "NMI detected").unwrap(); + // ClockState::HoldNmi { cycles: 5 } + // } else if !self.cpu.status.interrupt_disable() + // && (self.ppu.irq_waiting() || self.apu.irq_waiting()) + // { + // // TODO: handle proper irq detection + // writeln!(self.debug_log, "IRQ detected").unwrap(); + // ClockState::HoldIrq { cycles: 6 } + // } else + { let addr = self.cpu.pc; let instruction = self.read(self.cpu.pc); - self.cpu.pc = self.cpu.pc.wrapping_add(1); + if instruction != 0x02 { + self.cpu.pc = self.cpu.pc.wrapping_add(1); + } match self.exec_instruction(instruction, &[], false, addr) { ExecState::Done => ClockState::ReadInstruction, ExecState::MoreParams => ClockState::ReadOperands { @@ -2010,10 +2023,26 @@ impl NES { } } }; + if self.cpu.clock_state == ClockState::ReadInstruction { + if self.cpu.nmi_pending { + self.cpu.nmi_pending = false; + self.cpu.clock_state = ClockState::HoldNmi { cycles: 6 }; + } else if self.cpu.irq_pending && !self.cpu.status.interrupt_disable() { + self.cpu.clock_state = ClockState::HoldIrq { cycles: 6 }; + } + } + // Check NMI and IRQ line state. This happens in phi2, i.e. the second half of the instruction + let new_line_state = self.ppu.nmi_waiting() || self.apu.nmi_waiting(); + if new_line_state != self.cpu.nmi_line_state { + // println!("ppu: {}, apu: {}", self.ppu.nmi_waiting(), self.apu.nmi_waiting()); + // println!("New: {new_line_state}, old: {}", self.cpu.nmi_line_state); + self.cpu.nmi_line_state = new_line_state; + self.cpu.nmi_pending |= new_line_state; + } + self.cpu.irq_pending = self.ppu.irq_waiting() || self.apu.irq_waiting(); } fn cpu_cycle(&mut self) { - self.cycle += 1; match self.dma { DmaState::Passive => self.clock_cpu(), // TODO: Validate that this takes the correct number of cycles (513 or 514 cycles) @@ -2048,9 +2077,12 @@ impl NES { }; } } - if [0x8031, 0x8014].contains(&self.cpu.pc) { - self.dbg_int = true; + if !self.halted { + self.cycle += 1; } + // if [0x8031, 0x8014].contains(&self.cpu.pc) { + // self.dbg_int = true; + // } } pub fn run_one_clock_cycle(&mut self) -> CycleResult { @@ -2069,8 +2101,15 @@ impl NES { } else { false }; + if !self.halted { + let apu_exec = self.apu.run_one_clock_cycle(self.clock_count); + } + let ppu_frame = if !self.halted { + self.ppu.run_one_clock_cycle() + } else { + false + }; // 3 PPU clock cycles for each CPU clock cycle - let ppu_frame = self.ppu.run_one_clock_cycle(); let dbg_int = self.dbg_int | self.ppu.dbg_int; self.dbg_int = false; self.ppu.dbg_int = false; @@ -2095,7 +2134,8 @@ impl NES { // self.ppu.memory.clear(); *self = Self::from_rom( self.memory.rom(6).expect("PRG ROM"), - self.ppu.memory.rom(0).expect("CHR ROM"), + self.ppu.memory.rom_or_ram(0).expect("CHR ROM"), + self.mapper, ); self.reset(); } @@ -2108,8 +2148,36 @@ impl NES { } } - pub fn reset_and_run_with_timeout(&mut self, max_clock_cycles: usize) { - self.reset(); + /// Act as though the last STP instruction was a NOP. + /// + /// Clocks the PPU & APU forward once if appropriate + pub fn repl_nop(&mut self) { + assert!( + self.halted, + "This method may only be called when the CPU is halted" + ); + assert!( + // self.debug_log.pop().is_some_and(|s| s.contains("HLT")), + self.last_instruction.contains("HLT"), + "This method may only be called after a STP instruction" + ); + self.halted = false; + // Turn final STP into NOP + self.ppu.run_one_clock_cycle(); + self.apu.run_one_clock_cycle(self.clock_count); + self.cpu.clock_state = ClockState::Hold { + cycles: 0, + instruction: 0xEA, + ops: [0; 5], + count: 0, + addr: self.cpu.pc, + }; + self.cpu.pc += 1; + self.cycle += 1; + // self.clock_cpu(); + } + + pub fn run_with_timeout(&mut self, max_clock_cycles: usize) { let mut cur = 0; while !self.halted && cur < max_clock_cycles { // info!("Running clock cycle: {}", self.clock_count); @@ -2118,6 +2186,11 @@ impl NES { } } + pub fn reset_and_run_with_timeout(&mut self, max_clock_cycles: usize) { + self.reset(); + self.run_with_timeout(max_clock_cycles); + } + pub fn image(&self) -> &RenderBuffer<256, 240> { &self.ppu.render_buffer } @@ -2126,6 +2199,10 @@ impl NES { &self.ppu } + pub fn apu(&self) -> &APU { + &self.apu + } + pub fn cpu_mem(&self) -> &impl Memory { &self.memory } @@ -2137,4 +2214,8 @@ impl NES { pub fn debug_log_mut(&mut self) -> &mut DebugLog { &mut self.debug_log } + + pub fn halted(&self) -> bool { + self.halted + } } diff --git a/src/main.rs b/src/main.rs index 5195ca4..66ce7f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,17 +3,17 @@ use std::{collections::HashMap, fmt, time::Duration}; use iced::{ Color, Element, Font, Length::{Fill, Shrink}, - Point, Renderer, Size, Subscription, Task, Theme, mouse, + Point, Rectangle, Renderer, Size, Subscription, Task, Theme, mouse, widget::{ - Canvas, button, + Action, Canvas, button, canvas::{Frame, Program}, - column, container, row, text, + column, container, mouse_area, row, text, }, window::{self, Id, Settings}, }; use nes_emu::{ NES, PPU, - debugger::{DebuggerMessage, DebuggerState}, + debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image}, header_menu::header_menu, hex_view::{HexEvent, HexView}, resize_watcher::resize_watcher, @@ -22,8 +22,12 @@ use tokio::runtime::Runtime; use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes"); -const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes"); -// const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_red.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes"); +const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; +// const ROM_FILE: &str = "./cpu_timing_test.nes"; extern crate nes_emu; @@ -43,6 +47,7 @@ fn main() -> Result<(), iced::Error> { #[derive(Debug, Clone, PartialEq, Eq)] enum MemoryTy { Cpu, + PPU, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -70,6 +75,7 @@ impl fmt::Display for HeaderButton { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Open(WindowType::Memory(MemoryTy::Cpu, _)) => write!(f, "Open Memory Viewer"), + Self::Open(WindowType::Memory(MemoryTy::PPU, _)) => write!(f, "Open PPU Memory Viewer"), Self::Open(WindowType::TileMap) => write!(f, "Open TileMap Viewer"), Self::Open(WindowType::TileViewer) => write!(f, "Open Tile Viewer"), Self::Open(WindowType::Debugger) => write!(f, "Open Debugger"), @@ -96,6 +102,7 @@ enum Message { CPU, DebugInt, WindowClosed(Id), + WindowOpened(Id), Header(HeaderButton), Hex(Id, HexEvent), Debugger(DebuggerMessage), @@ -106,6 +113,16 @@ impl Emulator { fn new() -> (Self, Task) { let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file"); nes.reset(); + // TODO: remove + // let mut count = 10; + // while !nes.halted() { + // if nes.run_one_clock_cycle().ppu_frame { + // count -= 1; + // if count <= 0 { + // break; + // } + // } + // } let (win, task) = iced::window::open(Settings { min_size: None, ..Settings::default() @@ -154,6 +171,12 @@ impl Emulator { return iced::exit(); } } + Message::WindowOpened(_id) => { + // if let Some(WindowType::Main) = self.windows.get(&id) { + // // println!("Running resize"); + // return iced::window::resize(id, self.main_win_size); + // } + } Message::Header(HeaderButton::Open(w)) => { return self.open(w); } @@ -188,8 +211,7 @@ impl Emulator { fn subscriptions(&self) -> Subscription { Subscription::batch([ window::close_events().map(Message::WindowClosed), - // window::events().map(Message::Window), - // window::resize_events().map(Message::WindowResized), + window::open_events().map(Message::WindowOpened), ]) } @@ -214,26 +236,18 @@ impl Emulator { MemoryTy::Cpu => view .render(self.nes.cpu_mem()) .map(move |e| Message::Hex(win, e)), + MemoryTy::PPU => view + .render(self.nes.ppu().mem()) + .map(move |e| Message::Hex(win, e)), }; let content = column![hex].width(Fill).height(Fill); container(content).width(Fill).height(Fill).into() } - Some(WindowType::TileMap) => { - container(Canvas::new(DbgImage::NameTable(self.nes.ppu()))) - .width(Fill) - .height(Fill) - .into() - } + Some(WindowType::TileMap) => dbg_image(DbgImage::NameTable(self.nes.ppu())).into(), Some(WindowType::TileViewer) => { - container(Canvas::new(DbgImage::PatternTable(self.nes.ppu()))) - .width(Fill) - .height(Fill) - .into() + dbg_image(DbgImage::PatternTable(self.nes.ppu())).into() } - Some(WindowType::Palette) => container(Canvas::new(DbgImage::Palette(self.nes.ppu()))) - .width(Fill) - .height(Fill) - .into(), + Some(WindowType::Palette) => dbg_image(DbgImage::Palette(self.nes.ppu())).into(), Some(WindowType::Debugger) => { container(self.debugger.view(&self.nes).map(Message::Debugger)) .width(Fill) @@ -257,6 +271,7 @@ impl Emulator { [ HeaderButton::Open(WindowType::Debugger), HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, HexView {})), + HeaderButton::Open(WindowType::Memory(MemoryTy::PPU, HexView {})), HeaderButton::Open(WindowType::TileMap), HeaderButton::Open(WindowType::TileViewer), HeaderButton::Open(WindowType::Palette), @@ -302,33 +317,3 @@ impl Program for Emulator { vec![frame.into_geometry()] } } - -enum DbgImage<'a> { - NameTable(&'a PPU), - PatternTable(&'a PPU), - Palette(&'a PPU), -} - -impl Program for DbgImage<'_> { - type State = (); - - fn draw( - &self, - _state: &Self::State, - renderer: &Renderer, - _theme: &Theme, - _bounds: iced::Rectangle, - _cursor: mouse::Cursor, - ) -> Vec> { - // const SIZE: f32 = 2.; - let mut name_table_frame = - Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.)); - name_table_frame.scale(2.); - match self { - DbgImage::NameTable(ppu) => ppu.render_name_table(&mut name_table_frame), - DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame), - DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame), - } - vec![name_table_frame.into_geometry()] - } -} diff --git a/src/mem.rs b/src/mem.rs index 8327ae3..ce6b193 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -1,4 +1,4 @@ -use crate::hex_view::Memory; +use crate::{hex_view::Memory, ppu::PPUMMRegisters}; pub enum Value<'a, R> { Value(u8), @@ -71,7 +71,10 @@ pub struct MemoryMap { impl MemoryMap { pub fn new(segments: Vec>) -> Self { - Self { edit_ver: 0, segments } + Self { + edit_ver: 0, + segments, + } } pub fn read(&self, addr: u16) -> Value<'_, R> { // self.edit_ver += 1; @@ -88,12 +91,13 @@ impl MemoryMap { let offset = addr - segment.position; let s = &self.segments[*index]; self.read(s.position + offset % s.size) - } - // Data::Disabled() => todo!(), + } // Data::Disabled() => todo!(), }; } } - todo!("Open bus") + // TODO: Open bus + Value::Value(0) + // todo!("Open bus") } pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&R, u16, u8)) { @@ -111,8 +115,7 @@ impl MemoryMap { let index = *index; let s = &self.segments[index]; self.write(s.position + offset % s.size, val, reg_fn) - } - // Data::Disabled() => todo!(), + } // Data::Disabled() => todo!(), }; } } @@ -129,12 +132,50 @@ impl MemoryMap { } pub(crate) fn rom(&self, idx: usize) -> Option<&[u8]> { - if let Some(Segment { mem: Data::ROM(val), .. }) = self.segments.get(idx) { + if let Some(Segment { + mem: Data::ROM(val), + .. + }) = self.segments.get(idx) + { Some(val) } else { None } } + + pub(crate) fn rom_or_ram(&self, idx: usize) -> Option<&[u8]> { + if let Some(Segment { + mem: Data::ROM(val), + .. + }) = self.segments.get(idx) + { + Some(val) + } else if let Some(Segment { + mem: Data::RAM(_), .. + }) = self.segments.get(idx) + { + Some(&[]) + } else { + None + } + } + + // pub fn with_peek_reg<'s>(&'s self, f: impl (Fn(&R, u16) -> Option) + 's) -> impl Memory + 's { + // struct MemImpl<'a, R>(&'a MemoryMap, Box Option + 'a>); + // impl Memory for MemImpl<'_, R> { + // fn peek(&self, val: u16) -> Option { + // match self.0.read(val) { + // Value::Value(v) => Some(v), + // Value::Register { reg, offset } => self.1(reg, offset), + // } + // } + + // fn edit_ver(&self) -> usize { + // self.0.edit_ver() + // } + // } + // MemImpl(self, Box::new(f)) + // } } impl Memory for MemoryMap { @@ -149,8 +190,7 @@ impl Memory for MemoryMap { let offset = addr - segment.position; let s = &self.segments[*index]; self.peek(s.position + offset % s.size) - } - // Data::Disabled() => todo!(), + } // Data::Disabled() => todo!(), }; } } @@ -161,3 +201,50 @@ impl Memory for MemoryMap { self.edit_ver } } + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Mapper { + horizontal_name_table: bool, +} + +impl Mapper { + pub fn from_flags(flags: u8) -> Self { + if flags & 0b11111110 != 0 { + todo!("Support other mapper flags"); + } + Self { + horizontal_name_table: flags & (1 << 0) == 1, + } + } + + pub(crate) fn ppu_map(&self, rom: &[u8]) -> MemoryMap { + let chr = if rom.len() == 0 { + Segment::ram("CHR RAM", 0x0000, 0x2000) + } else { + Segment::rom("CHR ROM", 0x0000, rom) + }; + if self.horizontal_name_table { + MemoryMap::new(vec![ + chr, + Segment::ram("Internal VRAM", 0x2000, 0x400), + Segment::ram("Internal VRAM", 0x2400, 0x400), + Segment::mirror("Internal VRAM", 0x2800, 0x400, 1), + Segment::mirror("Internal VRAM", 0x2C00, 0x400, 2), + Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1), + Segment::reg("Palette Control", 0x3F00, 0x0020, PPUMMRegisters::Palette), + Segment::mirror("Mirror of Palette", 0x3F20, 0x00E0, 6), + ]) + } else { + MemoryMap::new(vec![ + chr, + Segment::ram("Internal VRAM", 0x2000, 0x400), + Segment::mirror("Internal VRAM", 0x2400, 0x400, 1), + Segment::ram("Internal VRAM", 0x2800, 0x400), + Segment::mirror("Internal VRAM", 0x2C00, 0x400, 3), + Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1), + Segment::reg("Palette Control", 0x3F00, 0x0020, PPUMMRegisters::Palette), + Segment::mirror("Mirror of Palette", 0x3F20, 0x00E0, 6), + ]) + } + } +} diff --git a/src/ppu.rs b/src/ppu.rs index 8a9d29c..3efd83b 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -1,3 +1,5 @@ +use std::fmt; + use iced::{ Point, Size, advanced::graphics::geometry::Renderer, @@ -6,10 +8,10 @@ use iced::{ use crate::{ hex_view::Memory, - mem::{MemoryMap, Segment}, + mem::{Mapper, MemoryMap, Segment}, }; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Color { pub r: u8, pub g: u8, @@ -22,6 +24,13 @@ impl Into for Color { } } +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RenderBuffer { buffer: Box<[Color]>, } @@ -45,6 +54,22 @@ impl RenderBuffer { pub fn clone_from(&mut self, other: &Self) { self.buffer.copy_from_slice(&other.buffer); } + + pub fn assert_eq(&self, other: &Self) { + // if self.buffer != other.buffer { + for y in 0..H { + for x in 0..W { + if self.read(y, x) != other.read(y, x) { + panic!( + "Rendered Buffers do not match\n Mismatch at ({x}, {y})\n Left: {}\n Right: {}", + self.read(y, x), + other.read(y, x) + ); + } + } + } + // } + } } pub(crate) enum PPUMMRegisters { @@ -69,37 +94,40 @@ enum BackgroundState { pub struct Background { /// Current vram address, 15 bits - v: u16, + pub v: u16, /// Temp vram address, 15 bits - t: u16, + pub t: u16, /// Fine X control, 3 bits - x: u8, + pub x: u8, /// Whether this is the first or second write to PPUSCROLL /// When false, writes to x - w: bool, + pub w: bool, /// When true, v is incremented by 32 after each read - vram_column: bool, + pub vram_column: bool, + pub second_pattern: bool, state: BackgroundState, - cur_nametable: u8, - cur_attr: u8, - cur_high: u8, - cur_low: u8, - cur_shift_high: u8, - cur_shift_low: u8, + pub cur_nametable: u8, + pub cur_attr: u8, + pub next_attr: u8, + pub next_attr_2: u8, + pub cur_high: u8, + pub cur_low: u8, + pub cur_shift_high: u32, + pub cur_shift_low: u32, } pub 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 grayscale: bool, + pub background_on_left_edge: bool, + pub sprites_on_left_edge: bool, + pub enable_background: bool, + pub enable_sprites: bool, + pub em_red: bool, + pub em_green: bool, + pub em_blue: bool, } const COLORS: &'static [Color; 0b100_0000] = &[ @@ -431,9 +459,9 @@ pub struct Palette { } impl Palette { - pub fn color(&self, idx: u8) -> Color { + pub fn color(&self, idx: u8, palette: u8) -> Color { debug_assert!(idx < 0x20, "Palette index out of range"); - self.colors[(self.ram[idx as usize] & 0x3F) as usize] + self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize] } } @@ -441,7 +469,7 @@ pub struct PPU { // registers: PPURegisters, frame_count: usize, nmi_enabled: bool, - nmi_waiting: bool, + // nmi_waiting: bool, pub even: bool, pub scanline: usize, pub pixel: usize, @@ -451,7 +479,7 @@ pub struct PPU { pub(crate) memory: MemoryMap, palette: Palette, - background: Background, + pub background: Background, oam: OAM, pub render_buffer: RenderBuffer<256, 240>, pub dbg_int: bool, @@ -477,7 +505,7 @@ impl std::fmt::Debug for PPU { } impl PPU { - pub fn with_chr_rom(rom: &[u8]) -> Self { + pub fn with_chr_rom(rom: &[u8], mapper: Mapper) -> Self { Self { cycle: 25, dbg_int: false, @@ -503,24 +531,19 @@ impl PPU { vblank: false, frame_count: 0, nmi_enabled: false, - nmi_waiting: false, + // nmi_waiting: false, even: false, 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, 0x0020, PPUMMRegisters::Palette), - Segment::mirror("Mirror of Palette", 0x3F20, 0x00E0, 3), - ]), + memory: mapper.ppu_map(rom), background: Background { v: 0, t: 0, x: 0, w: false, vram_column: false, + second_pattern: false, state: BackgroundState::NameTableBytePre, cur_high: 0, cur_low: 0, @@ -528,6 +551,8 @@ impl PPU { cur_shift_low: 0, cur_nametable: 0, cur_attr: 0, + next_attr: 0, + next_attr_2: 0, }, oam: OAM { mem: vec![0u8; 256], @@ -538,7 +563,7 @@ impl PPU { pub fn reset(&mut self) { *self = Self { memory: std::mem::replace(&mut self.memory, MemoryMap::new(vec![])), - ..Self::with_chr_rom(&[]) + ..Self::with_chr_rom(&[], Mapper::from_flags(0)) }; } @@ -561,6 +586,7 @@ impl PPU { 5 => panic!("ppuscroll is write-only"), 6 => panic!("ppuaddr is write-only"), 7 => { + // println!("Updating v for ppudata read"); let val = self .memory .read(self.background.v) @@ -568,10 +594,7 @@ impl PPU { PPUMMRegisters::Palette => self.palette.ram[off as usize], }); // if self.background - self.background.v = self - .background - .v - .wrapping_add(if self.background.vram_column { 32 } else { 1 }); + self.increment_v(); val } // 7 => self.registers.data, @@ -584,7 +607,9 @@ impl PPU { 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; + // self.background.vram_column = val & 0b0000_0010 != 0; + self.background.vram_column = val & 0b0000_0100 != 0; + self.background.second_pattern = val & 0b0001_0000 != 0; // TODO: other control fields } 0x01 => { @@ -628,20 +653,43 @@ impl PPU { if self.background.w { self.background.v = u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]); - self.background.w = true; + self.background.w = false; } else { self.background.v = u16::from_le_bytes([self.background.v.to_le_bytes()[0], val & 0b0011_1111]); self.background.w = true; } + // println!("Updating v for ppuaddr write: to {:04X}", self.background.v); + // self.dbg_int = true; // todo!("PPUADDR write") } 0x07 => { - // TODO: ppu data + // println!("Writing: {:02X}, @{:04X}", val, self.background.v); + self.memory + .write(self.background.v, val, |r, o, v| match r { + PPUMMRegisters::Palette => { + // println!("Writing {:02X} to {:02X}", v & 0x3F, o); + self.palette.ram[o as usize] = v & 0x3F; + } + }); + self.increment_v(); + // self.background.v += 1; // TODO: implement inc behavior } _ => panic!("No register at {:02X}", offset), } - // TODO: use data in PPU + } + /// Apply either row wise or column wise increment + fn increment_v(&mut self) { + if self.background.vram_column { + // if self.background.v + self.background.v += 32; + } else { + self.background.v += 1; + } + // self.background.v = self + // .background + // .v + // .wrapping_add(if self.background.vram_column { 32 } else { 1 }); } // pub fn write_oamdma(&mut self, val: u8) { // // TODO: OAM high addr @@ -662,90 +710,140 @@ impl PPU { 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 { + 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.pixel <= 304 && self.scanline == 261 { + self.background.v = (self.background.v & 0b0000_0100_0001_1111) + | (self.background.t & 0b0111_1011_1110_0000); + } + if self.pixel > 280 && self.pixel < 320 { + self.background.cur_shift_high <<= 1; + self.background.cur_shift_low <<= 1; + } // TODO if self.pixel == 0 { // self.dbg_int = true; // idle cycle - } else if self.pixel < 257 { + } else if self.pixel < 257 || self.pixel > 320 { // self.dbg_int = true; - if self.scanline < 240 { - // Determine background color - let a = self.background.cur_shift_high & 0x80; - let b = self.background.cur_shift_low & 0x80; - let val = (a >> 6) | (b >> 7); - debug_assert!(val < 4); + const POS: u32 = 1 << 15; + // Determine background color + let a = self.background.cur_shift_high & POS; + let b = self.background.cur_shift_low & POS; + let val = (a >> 14) | (b >> 15); + debug_assert!(val < 4); + let h_off = ((self.pixel - 1) / 16) % 2; + let v_off = (self.scanline / 16) % 2; + let off = v_off * 4 + h_off * 2; + let palette = (self.background.cur_attr >> off) & 0x3; + + if self.scanline < 240 && self.pixel < 257 { // Write to screen self.render_buffer.write( self.scanline, self.pixel - 1, - self.palette.color(val) - // self.palette.colors[val as usize], + self.palette + .color(val as u8, if val != 0 { palette } else { 0 }), // self.palette.colors[val as usize], ); // TODO: this should come from shift registers + } + if self.pixel < 337 { self.background.cur_shift_high <<= 1; self.background.cur_shift_low <<= 1; } - self.background.state = match self.background.state { - BackgroundState::NameTableByte => { - // TODO: Fetch name table byte - let addr = 0x2000 + self.pixel / 8 + self.scanline / 8; - let val = self.memory.read(addr as u16).reg_map(|_, _| todo!()); - self.background.cur_nametable = val; - // self.background.v; - BackgroundState::AttrTableBytePre + + if self.scanline < 240 || self.scanline == 261 { + if self.pixel <= 260 || self.pixel >= 321 { + if self.pixel % 8 == 2 { + // Name table fetch + // let addr = 0x2000 + self.pixel / 8 + self.scanline / 8; + let addr = 0x2000 | (self.background.v & 0x0FFF); + // println!("Cur: {:04X}, comp: {:04X}", addr, addr_2); + let val = self.memory.read(addr).reg_map(|_, _| 0); + self.background.cur_nametable = val; + } else if self.pixel % 8 == 4 { + // Attr table fetch + // let addr = 0x23C0 + self.pixel / 16 + self.scanline / 16; + let addr = 0x23C0 + | (self.background.v & 0x0C00) + | ((self.background.v >> 4) & 0x38) + | ((self.background.v >> 2) & 0x07); + // println!("Cur: {:04X}, comp: {:04X}", addr, addr_2); + // assert_eq!(addr, addr_2); + let val = self.memory.read(addr).reg_map(|_, _| 0); // TODO: handle reg reads + self.background.next_attr_2 = val; + } else if self.pixel % 8 == 6 { + // BG pattern low + let addr = self.background.cur_nametable as u16 * 16 + + ((self.background.v & 0x7000) >> 12) + // + (self.scanline % 8) as u16 + + if self.background.second_pattern { + 0x1000 + } else { + 0 + }; + let val = self.memory.read(addr).reg_map(|_, _| 0); + self.background.cur_low = val; + } else if self.pixel % 8 == 0 && self.pixel > 0 { + let addr = self.background.cur_nametable as u16 * 16 + + 8 + + ((self.background.v & 0x7000) >> 12) + // (self.scanline % 8) as u16 + + if self.background.second_pattern { + 0x1000 + } else { + 0 + }; + let val = self.memory.read(addr).reg_map(|_, _| todo!()); + self.background.cur_high = val; + self.background.cur_shift_low |= self.background.cur_low as u32; + self.background.cur_shift_high |= self.background.cur_high as u32; + self.background.cur_attr = self.background.next_attr; + self.background.next_attr = self.background.next_attr_2; + // Inc horizontal + if self.background.v & 0x1F == 31 { + self.background.v = (self.background.v & !0x1F) ^ 0x400; + } else { + self.background.v += 1; + } + // Inc vertical + if self.pixel == 256 { + // && self.scanline % 4 == 0 + if self.background.v & 0x7000 != 0x7000 { + self.background.v += 0x1000; + } else { + self.background.v &= !0x7000; + let mut y = (self.background.v & 0x03E0) >> 5; + if y == 29 { + y = 0; + self.background.v ^= 0x0800 + } else if y == 31 { + y = 0; + } else { + y += 1; + } + self.background.v = + (self.background.v & !0x03E0) | (y << 5); + } + } + } } - BackgroundState::AttrTableByte => { - // TODO: Fetch attr table byte - // let addr = 0x2000 + self.pixel / 8 + self.scanline / 8; - // let val = self.memory.read(addr as u16).reg_map(|_, _| todo!()); - // self.background.cur_attr = val; - BackgroundState::PatternTableTileLowPre - } - BackgroundState::PatternTableTileLow => { - // TODO: Fetch - let addr = self.background.cur_nametable as u16 * 16 - + (self.scanline % 8) as u16; - let val = self.memory.read(addr).reg_map(|_, _| todo!()); - self.background.cur_low = val; - BackgroundState::PatternTableTileHighPre - } - BackgroundState::PatternTableTileHigh => { - // TODO: Fetch - let addr = self.background.cur_nametable as u16 * 16 - + 8 - + (self.scanline % 8) as u16; - let val = self.memory.read(addr).reg_map(|_, _| todo!()); - self.background.cur_high = val; - self.background.cur_shift_low = self.background.cur_low; - self.background.cur_shift_high = self.background.cur_high; - 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 == 261 && self.pixel == 1 { + self.vblank = false; + // TODO: clear sprite 0 & sprite overflow + } if self.scanline == 241 && self.pixel == 1 { self.vblank = true; - self.nmi_waiting = self.nmi_enabled; + // self.nmi_waiting = self.nmi_enabled; self.frame_count += 1; self.background.state = BackgroundState::NameTableBytePre; return true; @@ -753,52 +851,80 @@ impl PPU { return false; } + pub fn nmi_on_vblank(&self) -> bool { + self.nmi_enabled + } pub fn peek_nmi(&self) -> bool { - self.nmi_waiting + self.vblank && self.nmi_enabled } pub fn nmi_waiting(&mut self) -> bool { - if self.nmi_waiting { - self.nmi_waiting = false; - return true; - } else { - return false; - } + self.vblank && self.nmi_enabled + // if self.nmi_waiting { + // self.nmi_waiting = false; + // return true; + // } else { + // return false; + // } } pub fn peek_irq(&self) -> bool { - self.nmi_waiting + false } pub fn irq_waiting(&mut self) -> bool { false } pub fn render_name_table(&self, frame: &mut Frame) { - 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 in 0..60 { + for x in 0..64 { + let row = y % 30; + let col = x % 32; + let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32); + let name = self.memory.peek((off + col + row * 32) as u16).unwrap() as u16 * 16 + + if self.background.second_pattern { + 0x1000 + } else { + 0 + }; + let attr = self + .memory + .peek((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16) + .unwrap(); + // attr << (((col & 1) << 1) | ((row & 1) << 0)) * 2 + // let h_off = ((self.pixel - 1) / 16) % 2; + // let v_off = (self.scanline / 16) % 2; + // let off = v_off * 4 + h_off * 2; + let palette = (attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3; 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() { + for bit in 0..8 { frame.fill_rectangle( - Point::new(x as f32 * 8. + bit as f32, y as f32 * 8. + y_off as f32), + 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, - }, + (false, false) => self.palette.color(0, 0), + (true, false) => self.palette.color(1, palette), + (false, true) => self.palette.color(2, palette), + (true, true) => self.palette.color(3, palette), + // (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, + // }, }, ); } @@ -808,6 +934,61 @@ impl PPU { } } } + pub fn name_cursor_info(&self, cursor: Point) -> Option { + let x = (cursor.x / 8.) as usize; + let y = (cursor.y / 8.) as usize; + if x < 64 && y < 60 { + let row = y % 30; + let col = x % 32; + let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32); + let name = self.memory.peek((off + col + row * 32) as u16).unwrap() as usize; + let attr = self + .memory + .peek((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16) + .unwrap(); + Some(format!( + "Row, Column: {}, {} +X, Y: {}, {} +Tilemap address: ${:04X} + +Tile Index: ${:02X} +Tile Address (PPU): ${:04X} +Tile Address (CHR): ${:04X} + +Palette Index {} +Palette Address ${:04X} + +Attribute Address ${:04X} +Attribute data: ${:02X} +", + row, + col, + col * 8, + row * 8, + off + col + row * 32, + name, + name * 16 + + if self.background.second_pattern { + 0x1000 + } else { + 0 + }, + name * 16 + + if self.background.second_pattern { + 0x1000 + } else { + 0 + }, + (attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3, + ((attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3) as usize * 4 + 0x3F00, + col / 4 + row / 4 * 8 + 0x3C0 + off, + attr, + )) + } else { + None + } + } + pub fn render_pattern_tables(&self, frame: &mut Frame) { for y in 0..16 { for x in 0..16 { @@ -817,7 +998,7 @@ impl PPU { let high = self.memory.peek(name + y_off + 8).unwrap(); for bit in 0..8 { frame.fill_rectangle( - Point::new( + Point::new( x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32, ), @@ -885,6 +1066,20 @@ impl PPU { } } } + pub fn pattern_cursor_info(&self, cursor: Point) -> Option { + let x = (cursor.x / 8.) as usize; + let y = (cursor.y / 8.) as usize; + if x < 16 && y < 32 { + Some(format!( + "Tile address (PPU): {:04X}\nTile address (CHR): {:04X}\nIndex: {:02X}", + (y * 16 + x) * 16, + (y * 16 + x) * 16, + ((y % 16) * 16 + x), + )) + } else { + None + } + } pub fn render_palette(&self, frame: &mut Frame) { for y in 0..8 { @@ -897,6 +1092,24 @@ impl PPU { } } } + pub fn palette_cursor_info(&self, cursor: Point) -> Option { + let x = (cursor.x / 10.) as usize; + let y = (cursor.y / 10.) as usize; + if x < 4 && y < 8 { + Some(format!( + "Index: {:02X}\nValue: {:02X}\nColor code: {}", + x + y * 4, + self.palette.ram[x + y * 4] & 0x3F, + self.palette.colors[(self.palette.ram[x + y * 4] & 0x3F) as usize], + )) + } else { + None + } + } + + pub fn mem(&self) -> &impl Memory { + &self.memory + } } #[cfg(test)] @@ -905,7 +1118,7 @@ mod tests { #[test] fn ppu_registers() { - let mut ppu = PPU::with_chr_rom(&[0u8; 8192]); + let mut ppu = PPU::with_chr_rom(&[0u8; 8192], Mapper::from_flags(0)); assert_eq!(ppu.background.v, 0); assert_eq!(ppu.background.t, 0); assert_eq!(ppu.background.x, 0); diff --git a/src/test_roms/asm6f b/src/test_roms/asm6f deleted file mode 100755 index 3fd2937bc10b3f286aa9606621b9f267572b1cc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66952 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 IBKV;BE3nx^djJ3c diff --git a/src/test_roms/basic-cpu-nop.s b/src/test_roms/basic-cpu-nop.s new file mode 100644 index 0000000..983abb9 --- /dev/null +++ b/src/test_roms/basic-cpu-nop.s @@ -0,0 +1,13 @@ +.include "testing.s" + +reset: + sed + nop + stp + +nmi: + stp + +irq: + stp + diff --git a/src/test_roms/basic-cpu.asm b/src/test_roms/basic-cpu.asm deleted file mode 100644 index 71c597c..0000000 --- a/src/test_roms/basic-cpu.asm +++ /dev/null @@ -1,26 +0,0 @@ -; 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" diff --git a/src/test_roms/basic-cpu.s b/src/test_roms/basic-cpu.s new file mode 100644 index 0000000..229421b --- /dev/null +++ b/src/test_roms/basic-cpu.s @@ -0,0 +1,13 @@ +.include "testing.s" + +reset: + sed + stp + stp + +nmi: + stp + +irq: + stp + diff --git a/src/test_roms/basic_init_0.asm b/src/test_roms/basic_init_0.s similarity index 59% rename from src/test_roms/basic_init_0.asm rename to src/test_roms/basic_init_0.s index 06e8a9e..12edc2e 100644 --- a/src/test_roms/basic_init_0.asm +++ b/src/test_roms/basic_init_0.s @@ -1,10 +1,6 @@ -.inesprg 2 ; 2 banks -.ineschr 1 ; -.inesmap 0 ; mapper 0 = NROM -.inesmir 0 ; background mirroring, horizontal +.include "testing.s" -.org $8000 -RESET: +reset: sei ; Ignore IRQs while starting up cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) ldx #$40 @@ -23,21 +19,11 @@ RESET: ; VBLANKWAIT2: ; bit $2002 ; bpl VBLANKWAIT2 - hlt - hlt + stp + stp -ERROR_: - hlt +nmi: + stp -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" +irq: + stp diff --git a/src/test_roms/basic_init_1.asm b/src/test_roms/basic_init_1.asm deleted file mode 100644 index 3794952..0000000 --- a/src/test_roms/basic_init_1.asm +++ /dev/null @@ -1,43 +0,0 @@ -.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" diff --git a/src/test_roms/basic_init_1.s b/src/test_roms/basic_init_1.s new file mode 100644 index 0000000..4a87cbe --- /dev/null +++ b/src/test_roms/basic_init_1.s @@ -0,0 +1,26 @@ +.include "testing.s" + +reset: + sei + 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 + stp + stp + +nmi: + stp + +irq: + stp diff --git a/src/test_roms/basic_init_2.asm b/src/test_roms/basic_init_2.s similarity index 59% rename from src/test_roms/basic_init_2.asm rename to src/test_roms/basic_init_2.s index 2545655..6dd16e0 100644 --- a/src/test_roms/basic_init_2.asm +++ b/src/test_roms/basic_init_2.s @@ -1,10 +1,6 @@ -.inesprg 2 ; 2 banks -.ineschr 1 ; -.inesmap 0 ; mapper 0 = NROM -.inesmir 0 ; background mirroring, horizontal +.include "testing.s" -.org $8000 -RESET: +reset: sei ; Ignore IRQs while starting up cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) ldx #$40 @@ -23,21 +19,11 @@ VBLANKWAIT1: VBLANKWAIT2: bit $2002 bpl VBLANKWAIT2 - hlt - hlt + stp + stp -ERROR_: - hlt +nmi: + stp -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" +irq: + stp diff --git a/src/test_roms/basic_init_3.asm b/src/test_roms/basic_init_3.s similarity index 61% rename from src/test_roms/basic_init_3.asm rename to src/test_roms/basic_init_3.s index 610edae..deea0be 100644 --- a/src/test_roms/basic_init_3.asm +++ b/src/test_roms/basic_init_3.s @@ -1,10 +1,6 @@ -.inesprg 2 ; 2 banks -.ineschr 1 ; -.inesmap 0 ; mapper 0 = NROM -.inesmir 0 ; background mirroring, horizontal +.include "testing.s" -.org $8000 -RESET: +reset: sei ; Ignore IRQs while starting up cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) ldx #$40 @@ -26,21 +22,9 @@ VBLANKWAIT2: VBLANKWAIT3: bit $2002 bpl VBLANKWAIT3 - hlt - hlt + stp +nmi: + stp -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" +irq: + stp diff --git a/src/test_roms/common/shell.inc b/src/test_roms/common/shell.inc index 0a750c1..437a9ab 100644 --- a/src/test_roms/common/shell.inc +++ b/src/test_roms/common/shell.inc @@ -1,14 +1,14 @@ ; Included at beginning of program .macro stp - .byte #$02 + .byte $02 .endmacro .include "macros.inc" .include "neshw.inc" -.ifdef CUSTOM_PREFIX - .include "custom_prefix.s" -.endif +; .ifdef CUSTOM_PREFIX +; .include "custom_prefix.s" +; .endif ; ; Devcart ; .ifdef BUILD_DEVCART @@ -16,9 +16,9 @@ ; .endif ; NES internal RAM -.ifdef BUILD_NOCART - .include "build_nocart.s" -.endif +; .ifdef BUILD_NOCART +; .include "build_nocart.s" +; .endif ; NES ROM (default) .ifndef SHELL_INCLUDED diff --git a/src/test_roms/common/testing.s b/src/test_roms/common/testing.s index ba41f03..793bf4e 100644 --- a/src/test_roms/common/testing.s +++ b/src/test_roms/common/testing.s @@ -1,106 +1,36 @@ -; Utilities for writing test ROMs - -; In NVRAM so these can be used before initializing runtime, -; then runtime initialized without clearing them -nv_res test_code ; code of current test -nv_res test_name,2 ; address of name of current test, or 0 of none - - -; Sets current test code and optional name. Also resets -; checksum. -; Preserved: A, X, Y -.macro set_test code,name - pha - lda #code - jsr set_test_ - .ifblank name - setb test_name+1,0 - .else - .local Addr - setw test_name,Addr - seg_data "RODATA",{Addr: .byte name,0} - .endif - pla +; testing.inc +; +.macro stp + .byte $02 +.endmacro +.macro zp_res name,size + .pushseg + .segment "ZEROPAGE" + name: + .ifblank size + .res 1 + .else + .res size + .endif + .popseg .endmacro -set_test_: - sta test_code - jmp reset_crc +.include "neshw.inc" +; ROM parts +.segment "HEADER" + .byte $4E,$45,$53,26 ; "NES" EOF + .byte 2,1 ; 32K PRG, 8K CHR + .byte $01 ; vertical mirroring +.segment "VECTORS" + .word $FFFF,$FFFF,$FFFF, nmi, reset, irq -; Initializes testing module -init_testing: - jmp init_crc - - -; Reports that all tests passed -tests_passed: - jsr print_filename - print_str newline,"Passed" - lda #0 - jmp exit - - -; Reports "Done" if set_test has never been used, -; "Passed" if set_test 0 was last used, or -; failure if set_test n was last used. -tests_done: - ldx test_code - jeq tests_passed - inx - bne test_failed - jsr print_filename - print_str newline,"Done" - lda #0 - jmp exit - - -; Reports that the current test failed. Prints code and -; name last set with set_test, or just "Failed" if none -; have been set yet. -test_failed: - ldx test_code - - ; Treat $FF as 1, in case it wasn't ever set - inx - bne :+ - inx - stx test_code -: - ; If code >= 2, print name - cpx #2-1 ; -1 due to inx above - blt :+ - lda test_name+1 - beq :+ - jsr print_newline - sta addr+1 - lda test_name - sta addr - jsr print_str_addr - jsr print_newline -: - jsr print_filename - - ; End program - lda test_code - jmp exit - - -; If checksum doesn't match expected, reports failed test. -; Clears checksum afterwards. -; Preserved: A, X, Y -.macro check_crc expected - jsr_with_addr check_crc_,{.dword expected} +.macro patterns_bin name + .pushseg + .segment "CHARS" + .incbin name + .popseg .endmacro -check_crc_: - pha - jsr is_crc_ - bne :+ - jsr reset_crc - pla - rts +.segment "CODE" -: jsr print_newline - jsr print_crc - jmp test_failed diff --git a/src/test_roms/even_odd.asm b/src/test_roms/even_odd.s similarity index 59% rename from src/test_roms/even_odd.asm rename to src/test_roms/even_odd.s index cae4467..7cad08f 100644 --- a/src/test_roms/even_odd.asm +++ b/src/test_roms/even_odd.s @@ -1,10 +1,6 @@ -.inesprg 2 ; 2 banks -.ineschr 1 ; -.inesmap 0 ; mapper 0 = NROM -.inesmir 0 ; background mirroring, horizontal +.include "testing.s" -.org $8000 -RESET: +reset: sei ; Ignore IRQs while starting up cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) ldx #$40 @@ -24,21 +20,11 @@ VBLANKWAIT1: VBLANKWAIT2: bit $2002 bpl VBLANKWAIT2 - hlt - hlt + stp + stp -ERROR_: - hlt +nmi: + stp -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" +irq: + stp diff --git a/src/test_roms/int_nmi_exit_timing.s b/src/test_roms/int_nmi_exit_timing.s new file mode 100644 index 0000000..1c8fee7 --- /dev/null +++ b/src/test_roms/int_nmi_exit_timing.s @@ -0,0 +1,38 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +zp_res FLAG + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta FLAG + + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + lda FLAG + beq loop + stp + +nmi: + lda #$01 + sta FLAG + rti + +irq: + stp diff --git a/src/test_roms/int_nmi_timing.s b/src/test_roms/int_nmi_timing.s new file mode 100644 index 0000000..ea2aeb3 --- /dev/null +++ b/src/test_roms/int_nmi_timing.s @@ -0,0 +1,31 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +nmi: + nop + stp + rti + +irq: + stp diff --git a/src/test_roms/int_nmi_while_nmi.s b/src/test_roms/int_nmi_while_nmi.s new file mode 100644 index 0000000..8e4df98 --- /dev/null +++ b/src/test_roms/int_nmi_while_nmi.s @@ -0,0 +1,39 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +zp_res FLAG + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta FLAG + + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + lda FLAG + beq loop + stp + +nmi: + lda PPUSTATUS +l: + jmp l + rti + +irq: + stp diff --git a/src/test_roms/interrupts.rs b/src/test_roms/interrupts.rs new file mode 100644 index 0000000..ab9a9ea --- /dev/null +++ b/src/test_roms/interrupts.rs @@ -0,0 +1,27 @@ +use crate::hex_view::Memory; + +use super::rom_test; + +rom_test!(int_nmi_timing, "int_nmi_timing.nes", |nes| { + assert_eq!(nes.last_instruction, "0x801B HLT :2 []"); + assert_eq!(nes.clock_count, 260881); + assert_eq!(nes.cycle, 86967); + assert_eq!(nes.ppu().pixel, 40); + assert_eq!(nes.ppu().scanline, 241); + assert_eq!(nes.ppu().cycle, 260905); + assert_eq!(nes.cpu_mem().peek(0x1FF), Some(0x80)); + assert_eq!(nes.cpu_mem().peek(0x1FE), Some(0x17)); + assert_eq!(nes.cpu_mem().peek(0x1FD), Some(0xA4)); +}); + +rom_test!(int_nmi_exit_timing, "int_nmi_exit_timing.nes", |nes| { + assert_eq!(nes.last_instruction, "0x801F HLT :2 []"); + // assert_eq!(nes.clock_count, 260881); + assert_eq!(nes.cycle, 86980); + assert_eq!(nes.ppu().pixel, 79); + assert_eq!(nes.ppu().scanline, 241); + assert_eq!(nes.ppu().cycle, 260905 - 40 + 79); + assert_eq!(nes.cpu_mem().peek(0x1FF), Some(0x80)); + assert_eq!(nes.cpu_mem().peek(0x1FE), Some(0x1B)); + assert_eq!(nes.cpu_mem().peek(0x1FD), Some(0x26)); +}); diff --git a/src/test_roms/mod.rs b/src/test_roms/mod.rs index 23cfe6e..dd38002 100644 --- a/src/test_roms/mod.rs +++ b/src/test_roms/mod.rs @@ -1,5 +1,7 @@ mod cpu_reset_ram; mod instr_test_v3; +mod ppu; +mod interrupts; use crate::hex_view::Memory; @@ -38,23 +40,35 @@ pub(crate) use rom_test; 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); - // Off by one from Mesen, since Mesen doesn't count the clock cycle attempting to execute the 'invalid' opcode - assert_eq!(nes.ppu.pixel, 35); + assert_eq!(nes.cycle, 10); + assert_eq!(nes.cpu.pc, 0x8001); + assert_eq!(nes.ppu.pixel, 34); + + assert_eq!(nes.cpu.sp, 0xFD); + + nes.repl_nop(); + nes.run_with_timeout(200); + assert_eq!(nes.last_instruction, "0x8002 HLT :2 []"); + assert_eq!(nes.cycle, 12); + assert_eq!(nes.cpu.pc, 0x8002); + assert_eq!(nes.ppu.pixel, 40); + + assert_eq!(nes.cpu.sp, 0xFD); +}); + +rom_test!(basic_cpu_with_nop, "basic-cpu-nop.nes", |nes| { + assert_eq!(nes.last_instruction, "0x8002 HLT :2 []"); + assert_eq!(nes.cycle, 12); + assert_eq!(nes.cpu.pc, 0x8002); + assert_eq!(nes.ppu.pixel, 40); 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.last_instruction, "0x800C HLT :2 []"); + assert_eq!(nes.cycle, 25); + assert_eq!(nes.cpu.pc, 0x800C); assert_eq!(nes.cpu.sp, 0xFD); assert_eq!(nes.cpu.a, 0xAA); @@ -67,8 +81,8 @@ rom_test!(read_write, "read_write.nes", |nes| { 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.cycle, 40); + assert_eq!(nes.cpu.pc, 0x8017); assert_eq!(nes.cpu.sp, 0xFF); assert_eq!(nes.cpu.a, 0x00); @@ -78,9 +92,9 @@ rom_test!(basic_init_0, "basic_init_0.nes", |nes| { 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.cpu.pc, 0x801D); - assert_eq!(nes.ppu.pixel, 30); + assert_eq!(nes.cycle, 27402); + assert_eq!(nes.cpu.pc, 0x801C); + assert_eq!(nes.ppu.pixel, 29); assert_eq!(nes.cpu.sp, 0xFF); assert_eq!(nes.cpu.a, 0x00); @@ -90,9 +104,9 @@ rom_test!(basic_init_1, "basic_init_1.nes", |nes| { rom_test!(basic_init_2, "basic_init_2.nes", |nes| { assert_eq!(nes.last_instruction, "0x8021 HLT :2 []"); - assert_eq!(nes.cycle, 57180); - assert_eq!(nes.cpu.pc, 0x8022); - assert_eq!(nes.ppu.pixel, 19); + assert_eq!(nes.cycle, 57179); + assert_eq!(nes.cpu.pc, 0x8021); + assert_eq!(nes.ppu.pixel, 18); assert_eq!(nes.cpu.sp, 0xFF); assert_eq!(nes.cpu.a, 0x00); @@ -102,9 +116,9 @@ rom_test!(basic_init_2, "basic_init_2.nes", |nes| { rom_test!(basic_init_3, "basic_init_3.nes", |nes| { assert_eq!(nes.last_instruction, "0x8026 HLT :2 []"); - assert_eq!(nes.cycle, 86964); - assert_eq!(nes.cpu.pc, 0x8027); - assert_eq!(nes.ppu.pixel, 29); + assert_eq!(nes.cycle, 86963); + assert_eq!(nes.cpu.pc, 0x8026); + assert_eq!(nes.ppu.pixel, 28); assert_eq!(nes.cpu.sp, 0xFF); assert_eq!(nes.cpu.a, 0x00); @@ -114,9 +128,9 @@ rom_test!(basic_init_3, "basic_init_3.nes", |nes| { rom_test!(even_odd, "even_odd.nes", |nes| { assert_eq!(nes.last_instruction, "0x8023 HLT :2 []"); - assert_eq!(nes.cycle, 57182); - assert_eq!(nes.cpu.pc, 0x8024); - assert_eq!(nes.ppu.pixel, 25); + assert_eq!(nes.cycle, 57181); + assert_eq!(nes.cpu.pc, 0x8023); + assert_eq!(nes.ppu.pixel, 24); assert_eq!(nes.cpu.sp, 0xFF); assert_eq!(nes.cpu.a, 0x00); diff --git a/src/test_roms/nes.cfg b/src/test_roms/nes.cfg index f75935c..f584cf8 100644 --- a/src/test_roms/nes.cfg +++ b/src/test_roms/nes.cfg @@ -12,7 +12,7 @@ MEMORY FF00: start = $FF00, size = $F4, fill=yes, fillval=$FF; VECTORS:start = $FFF4, size = $C, fill=yes; - CHARS: start = 0, size = $3000, fillval=$FF; + CHARS: start = 0, size = $3000, fill=yes, fillval=$FF; } SEGMENTS diff --git a/src/test_roms/pat.bin b/src/test_roms/pat.bin new file mode 100644 index 0000000000000000000000000000000000000000..2cba9c7dede48d91bd339ab2e74422bd52e8dafc GIT binary patch literal 64 QcmezWA3I=xf=~hg0G96}i2wiq literal 0 HcmV?d00001 diff --git a/src/test_roms/ppu.rs b/src/test_roms/ppu.rs new file mode 100644 index 0000000..8a74da5 --- /dev/null +++ b/src/test_roms/ppu.rs @@ -0,0 +1,90 @@ +use crate::{hex_view::Memory, Color, RenderBuffer}; + +use super::rom_test; + +const COLOR_01: Color = Color { + r: 0x00, + g: 0x2A, + b: 0x88, +}; +const COLOR_16: Color = Color { + r: 0xB5, + g: 0x31, + b: 0x20, +}; +const COLOR_0B: Color = Color { + r: 0x00, + g: 0x4F, + b: 0x08, +}; + +rom_test!(ppu_fill, "ppu_fill.nes", |nes| { + assert_eq!(nes.last_instruction, "0x802B HLT :2 []"); + assert_eq!(nes.cpu.a, 0x01); + + let mut buffer = RenderBuffer::empty(); + for l in 0..240 { + for p in 0..256 { + buffer.write(l, p, COLOR_01); + } + } + nes.ppu().render_buffer.assert_eq(&buffer); +}); + +rom_test!(ppu_fill_red, "ppu_fill_red.nes", |nes| { + assert_eq!(nes.last_instruction, "0x803A HLT :2 []"); + assert_eq!(nes.cpu.a, 0x01); + + let mut buffer = RenderBuffer::empty(); + for l in 0..240 { + for p in 0..256 { + buffer.write(l, p, COLOR_16); + } + } + nes.ppu().render_buffer.assert_eq(&buffer); +}); + +rom_test!(ppu_fill_palette_1, "ppu_fill_palette_1.nes", |nes| { + assert_eq!(nes.last_instruction, "0x8064 HLT :2 []"); + assert_eq!(nes.cpu.a, 0x01); + + let mut buffer = RenderBuffer::empty(); + for l in 0..240 { + for p in 0..256 { + if (p / 16) % 2 == (l / 16) % 2 { + buffer.write(l, p, COLOR_01); + } else { + buffer.write(l, p, COLOR_16); + } + } + } + nes.ppu().render_buffer.assert_eq(&buffer); +}); + +rom_test!(ppu_fill_name_table, "ppu_fill_name_table.nes", |nes| { + assert_eq!(nes.last_instruction, "0x80B3 HLT :2 []"); + assert_eq!(nes.cpu.a, 0x01); + + let mut buffer = RenderBuffer::empty(); + for l in 0..240 { + for p in 0..256 { + if p % 2 == l % 2 { + buffer.write(l, p, COLOR_01); + } else if (p / 16) % 2 == (l / 16) % 2 { + buffer.write(l, p, COLOR_0B); + } else { + buffer.write(l, p, COLOR_16); + } + } + } + nes.ppu().render_buffer.assert_eq(&buffer); +}); + +rom_test!(ppu_vertical_write, "ppu_vertical_write.nes", |nes| { + assert_eq!(nes.last_instruction, "0x8040 HLT :2 []"); + + assert_eq!(nes.ppu().mem().peek(0x2000), Some(1)); + assert_eq!(nes.ppu().mem().peek(0x2020), Some(1)); + assert_eq!(nes.ppu().mem().peek(0x2400), Some(2)); + assert_eq!(nes.ppu().mem().peek(0x2401), Some(2)); +}); diff --git a/src/test_roms/ppu_fill.s b/src/test_roms/ppu_fill.s new file mode 100644 index 0000000..7b5aa86 --- /dev/null +++ b/src/test_roms/ppu_fill.s @@ -0,0 +1,44 @@ +; Verifies that reset doesn't alter any RAM. + +CUSTOM_RESET=1 +.include "testing.s" + +; nv_res bad_addr,2 + +; PPUSTATUS = $2002 +zp_res COUNT + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + lda #$0A + sta PPUMASK; No EM, only background rendering, normal color + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +nmi: + lda COUNT + bne exit + inc COUNT + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/ppu_fill_name_table.s b/src/test_roms/ppu_fill_name_table.s new file mode 100644 index 0000000..c26e626 --- /dev/null +++ b/src/test_roms/ppu_fill_name_table.s @@ -0,0 +1,137 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" +; patterns_bin "pat.bin" +.macro chr b,s +.repeat s +.byte b +.endrepeat +.endmacro +.pushseg +.segment "CHARS" +chr $FF,16 ; Full +chr $00,8 +chr $FF,8 ; Gray +chr $55,16 ; Vertical stripes +chr $AA,16 ; Vertical stripes - reverse +.repeat 8 +.byte $FF +.byte $00 +.endrepeat ; Horizontal stripes +.repeat 8 +.byte $00 +.byte $FF +.endrepeat ; Horizontal stripes - reverse +.repeat 8 +.byte $55 +.byte $AA +.endrepeat ; Checkerboard +.repeat 8 +.byte $AA +.byte $55 +.endrepeat ; Checkerboard - reverse +.popseg + +zp_res COUNT + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + lda #$3F + sta PPUADDR + lda #$00 + sta PPUADDR ; Load $3F03 + lda #$01 + sta PPUDATA ; Load $16 into palette 0, color 0 + lda #$16 + sta PPUDATA ; Load $16 into palette 0, color 1 + sta PPUDATA ; Load $16 into palette 0, color 2 + sta PPUDATA ; Load $16 into palette 0, color 3 + lda #$0E + sta PPUDATA ; Load $16 into palette 0, color 0 + lda #$0B + sta PPUDATA ; Load $16 into palette 0, color 1 + sta PPUDATA ; Load $16 into palette 0, color 2 + sta PPUDATA ; Load $16 into palette 0, color 3 + lda #$0E + sta PPUDATA ; Load $16 into palette 0, color 0 + lda #$0B + sta PPUDATA ; Load $16 into palette 0, color 1 + sta PPUDATA ; Load $16 into palette 0, color 2 + sta PPUDATA ; Load $16 into palette 0, color 3 + lda #$0E + sta PPUDATA ; Load $16 into palette 0, color 0 + lda #$0B + sta PPUDATA ; Load $16 into palette 0, color 1 + sta PPUDATA ; Load $16 into palette 0, color 2 + sta PPUDATA ; Load $16 into palette 0, color 3 + + lda #$20 + sta PPUADDR + lda #$00 + sta PPUADDR ; Load $2000 + ;lda #$01 + ;sta PPUDATA ; Load $01 into name table 1 + lda #$06 + ldx #$00 +fill_loop_0: + sta PPUDATA ; Load $01 into name table 1 + dex + bne fill_loop_0 + ldx #$00 +fill_loop_1: + sta PPUDATA ; Load $01 into name table 1 + dex + bne fill_loop_1 + ldx #$00 +fill_loop_2: + sta PPUDATA ; Load $01 into name table 1 + dex + bne fill_loop_2 + ldx #$C0 +fill_loop_3: + sta PPUDATA ; Load $01 into name table 1 + dex + bne fill_loop_3 + lda #$41 + ldx #$40 +fill_loop: + sta PPUDATA ; Load $16 into palette 0, color 0 + dex + bne fill_loop + + lda #$0A + sta PPUMASK; No EM, only background rendering, normal color + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +nmi: + lda COUNT + bne exit + inc COUNT + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/ppu_fill_palette_1.s b/src/test_roms/ppu_fill_palette_1.s new file mode 100644 index 0000000..693b091 --- /dev/null +++ b/src/test_roms/ppu_fill_palette_1.s @@ -0,0 +1,67 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +zp_res COUNT + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + lda #$3F + sta PPUADDR + lda #$03 + sta PPUADDR ; Load $3F03 + lda #$16 + sta PPUDATA ; Load $16 into palette 0, color 3 + lda #$01 + sta PPUDATA ; Load $01 into palette 1 + sta PPUDATA ; Load $01 into palette 1 + sta PPUDATA ; Load $01 into palette 1 + sta PPUDATA ; Load $01 into palette 1 + + lda #$23 + sta PPUADDR + lda #$C0 + sta PPUADDR ; Load $23C0 + lda #$41 + ldx #$40 +fill_loop: + sta PPUDATA ; Load $16 into palette 0, color 0 + dex + bne fill_loop + + lda #$0A + sta PPUMASK; No EM, only background rendering, normal color + + lda #$00 + sta PPUSCROLL + sta PPUSCROLL + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +nmi: + lda COUNT + bne exit + inc COUNT + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/ppu_fill_red.s b/src/test_roms/ppu_fill_red.s new file mode 100644 index 0000000..0c9c330 --- /dev/null +++ b/src/test_roms/ppu_fill_red.s @@ -0,0 +1,46 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +zp_res COUNT + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses + lda #$3F + sta PPUADDR + lda #$03 + sta PPUADDR ; Load $3F03 + lda #$16 + sta PPUDATA ; Load $16 into palette 0, color 0 + lda #$0A + sta PPUMASK; No EM, only background rendering, normal color + lda #$80 + sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses +loop: + jmp loop + +nmi: + lda COUNT + bne exit + inc COUNT + rti +exit: + stp + +irq: + stp diff --git a/src/test_roms/ppu_vertical_write.s b/src/test_roms/ppu_vertical_write.s new file mode 100644 index 0000000..b40a054 --- /dev/null +++ b/src/test_roms/ppu_vertical_write.s @@ -0,0 +1,48 @@ +; Verifies that reset doesn't alter any RAM. + +.include "testing.s" + +zp_res COUNT + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + lda #$04 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses, vertical write + lda #$20 + sta PPUADDR + lda #$00 + sta PPUADDR ; Load $2000 + ldx #$01 + stx PPUDATA ; Load values into name table + stx PPUDATA ; Load values into name table + + lda #$00 + sta PPUCTRL ; NMI off, PPU slave, Small sprites, 0 addresses, normal write + lda #$24 + sta PPUADDR + lda #$00 + sta PPUADDR ; Load $2000 + ldx #$02 + stx PPUDATA ; Load values into name table + stx PPUDATA ; Load values into name table + + stp + +nmi: + stp + +irq: + stp diff --git a/src/test_roms/read_write.asm b/src/test_roms/read_write.asm deleted file mode 100644 index 8539af6..0000000 --- a/src/test_roms/read_write.asm +++ /dev/null @@ -1,29 +0,0 @@ -.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" diff --git a/src/test_roms/read_write.s b/src/test_roms/read_write.s new file mode 100644 index 0000000..a8b5505 --- /dev/null +++ b/src/test_roms/read_write.s @@ -0,0 +1,17 @@ + +.include "testing.s" + +reset: + lda #$aa + sta $0000 + ldx $0000 + ldy $0000 + stx $0001 + sty $0002 + stp + +nmi: + stp + +irq: + stp