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 3fd2937..0000000 Binary files a/src/test_roms/asm6f and /dev/null differ 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 0000000..2cba9c7 Binary files /dev/null and b/src/test_roms/pat.bin differ 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