Major work
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 10s

This commit is contained in:
2026-01-19 01:36:58 -06:00
parent c535e4e76d
commit cd3de5e361
34 changed files with 1750 additions and 606 deletions

View File

@@ -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
}
}

View File

@@ -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<String> {
self.history.pop()
}
}
impl std::fmt::Write for DebugLog {

View File

@@ -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<Element<'a, Message>>,
content2: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![
widget::container(text(label)).padding(2.),
widget::container(content).style(bordered_box).padding(2.),
widget::container(content2).style(bordered_box).padding(2.),
]
.spacing(1.)
.into()
}
pub fn labelled_box<'a, Message: 'a>(label: &'a str, value: bool) -> Element<'a, Message> {
row![checkbox(value), widget::container(text(label)),]
.spacing(1.)
.into()
}
#[derive(Clone, Copy)]
pub enum DbgImage<'a> {
NameTable(&'a PPU),
PatternTable(&'a PPU),
Palette(&'a PPU),
}
impl<Message, T> Program<Message, T> for DbgImage<'_> {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &T,
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// const SIZE: f32 = 2.;
let mut name_table_frame =
Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.));
name_table_frame.scale(2.);
// println!("Position: {:?}", cursor.position());
match self {
DbgImage::NameTable(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<String> {
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<DbgImage<'a>, M, T>,
text: Text<'a, T>,
padding: f32,
drawn: bool,
// image: DbgImage<'a>,
// text: te,
}
// pub trait Container<Message, Theme, Renderer> {
// // Not ideal
// fn children(&self) -> &[&dyn Widget<Message, Theme, Renderer>];
// }
impl<'s, Message, Theme> Widget<Message, Theme, Renderer> for DbgImageSetup<'s, Message, Theme>
where
Theme: text::Catalog + 's,
{
fn size(&self) -> Size<iced::Length> {
// self.
Size::new(Fill, Fill)
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &iced::advanced::layout::Limits,
) -> Node {
let img_node = self.image.layout(&mut tree.children[0], renderer, limits);
let txt_node = Widget::<Message, Theme, _>::layout(
&mut self.text,
&mut tree.children[1],
renderer,
&limits.shrink(Size::new(
img_node.size().width + self.padding * 2.,
self.padding * 2.,
)),
)
.move_to(Point::new(
img_node.size().width + self.padding,
self.padding,
));
Node::with_children(limits.max(), vec![img_node, txt_node])
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced::advanced::renderer::Style,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
self.image.draw(
&tree.children[0],
renderer,
theme,
style,
layout.child(0),
cursor,
viewport,
);
Widget::<Message, Theme, _>::draw(
&self.text,
&tree.children[1],
renderer,
theme,
style,
layout.child(1),
cursor,
viewport,
)
}
fn tag(&self) -> Tag {
Tag::of::<Rc<String>>()
}
fn state(&self) -> State {
State::new(Rc::new(String::new()))
}
fn children(&self) -> Vec<Tree> {
vec![
Tree::new(&self.image as &dyn Widget<Message, Theme, Renderer>),
Tree::new(&self.text as &dyn Widget<Message, Theme, Renderer>),
]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&[
&self.image as &dyn Widget<Message, Theme, Renderer>,
&self.text as &dyn Widget<Message, Theme, Renderer>,
]);
}
fn operate(
&mut self,
tree: &mut Tree,
layout: iced::advanced::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced::advanced::widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |op| {
self.image
.operate(&mut tree.children[0], layout.child(0), renderer, op);
Widget::<Message, Theme, _>::operate(
&mut self.text,
&mut tree.children[1],
layout.child(1),
renderer,
op,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &iced::Event,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn iced::advanced::Clipboard,
shell: &mut iced::advanced::Shell<'_, Message>,
viewport: &iced::Rectangle,
) {
self.image.update(
&mut tree.children[0],
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if matches!(event, iced::Event::Mouse(mouse::Event::CursorMoved { .. })) || !self.drawn {
if let Some(help) = cursor
.position_in(layout.child(0).bounds())
.and_then(|pos| self.dbg.help(Point::new(pos.x / 2., pos.y / 2.)))
{
self.text = text(help);
shell.invalidate_layout();
shell.request_redraw();
}
self.drawn = true;
}
Widget::<Message, Theme, _>::update(
&mut self.text,
&mut tree.children[1],
event,
layout.child(1),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
_tree: &iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'_>,
_cursor: iced::advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> iced::advanced::mouse::Interaction {
iced::advanced::mouse::Interaction::default()
}
fn overlay<'a>(
&'a mut self,
_tree: &'a mut iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'a>,
_renderer: &Renderer,
_viewport: &iced::Rectangle,
_translation: iced::Vector,
) -> Option<iced::advanced::overlay::Element<'a, Message, Theme, Renderer>> {
None
}
}
pub fn dbg_image<'a, Message: 'a>(img: DbgImage<'a>) -> Element<'a, Message> {
Element::new(DbgImageSetup {
dbg: img,
image: Canvas::new(img).width(img.width()).height(img.height()),
text: text(""),
padding: 10.,
drawn: false,
})
}

View File

@@ -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<CPUMMRegisters>,
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
}
}

View File

@@ -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<Message>) {
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<Message> {
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<Message> for Emulator {
vec![frame.into_geometry()]
}
}
enum DbgImage<'a> {
NameTable(&'a PPU),
PatternTable(&'a PPU),
Palette(&'a PPU),
}
impl Program<Message> for DbgImage<'_> {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// const SIZE: f32 = 2.;
let mut name_table_frame =
Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.));
name_table_frame.scale(2.);
match self {
DbgImage::NameTable(ppu) => ppu.render_name_table(&mut name_table_frame),
DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame),
DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame),
}
vec![name_table_frame.into_geometry()]
}
}

View File

@@ -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<R> {
impl<R> MemoryMap<R> {
pub fn new(segments: Vec<Segment<R>>) -> 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<R> MemoryMap<R> {
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<R> MemoryMap<R> {
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<R> MemoryMap<R> {
}
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<u8>) + 's) -> impl Memory + 's {
// struct MemImpl<'a, R>(&'a MemoryMap<R>, Box<dyn Fn(&R, u16) -> Option<u8> + 'a>);
// impl<R> Memory for MemImpl<'_, R> {
// fn peek(&self, val: u16) -> Option<u8> {
// 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<R> Memory for MemoryMap<R> {
@@ -149,8 +190,7 @@ impl<R> Memory for MemoryMap<R> {
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<R> Memory for MemoryMap<R> {
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<PPUMMRegisters> {
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),
])
}
}
}

View File

@@ -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<Fill> 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<const W: usize, const H: usize> {
buffer: Box<[Color]>,
}
@@ -45,6 +54,22 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
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<PPUMMRegisters>,
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<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..280 / 8 {
for x in 0..512 / 8 {
let name = self.memory.peek(x + y * 512 / 8 + 0x2000).unwrap() as u16 * 16;
for y 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<String> {
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<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..16 {
for x in 0..16 {
@@ -885,6 +1066,20 @@ impl PPU {
}
}
}
pub fn pattern_cursor_info(&self, cursor: Point) -> Option<String> {
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<R: Renderer>(&self, frame: &mut Frame<R>) {
for y in 0..8 {
@@ -897,6 +1092,24 @@ impl PPU {
}
}
}
pub fn palette_cursor_info(&self, cursor: Point) -> Option<String> {
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);

Binary file not shown.

View File

@@ -0,0 +1,13 @@
.include "testing.s"
reset:
sed
nop
stp
nmi:
stp
irq:
stp

View File

@@ -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"

13
src/test_roms/basic-cpu.s Normal file
View File

@@ -0,0 +1,13 @@
.include "testing.s"
reset:
sed
stp
stp
nmi:
stp
irq:
stp

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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));
});

View File

@@ -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);

View File

@@ -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

BIN
src/test_roms/pat.bin Normal file

Binary file not shown.

90
src/test_roms/ppu.rs Normal file
View File

@@ -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));
});

44
src/test_roms/ppu_fill.s Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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