Compare commits

...

3 Commits

Author SHA1 Message Date
f84009aeb9 Minor improvements to APU
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s
2026-04-08 21:16:54 -05:00
875e493cd2 Fix memory mapping for MMC1 2026-04-08 21:15:45 -05:00
d140f1e122 Fix minor warnings and debug log 2026-04-08 21:15:15 -05:00
14 changed files with 407 additions and 183 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
*.nes *.nes
*.zip *.zip
*.dmp *.dmp
wasm/
!wasm/wasm.html

View File

@@ -1,7 +1,3 @@
use std::iter::repeat_n;
use tracing::debug;
macro_rules! lut { macro_rules! lut {
($name:ident: [$ty:ty; $len:expr] = |$n:ident| $expr:expr) => { ($name:ident: [$ty:ty; $len:expr] = |$n:ident| $expr:expr) => {
const $name: [$ty; $len] = { const $name: [$ty; $len] = {
@@ -264,7 +260,7 @@ Output: {16:>4} ${16:02X}
self.sweep.period(), self.sweep.period(),
self.sweep.enable(), self.sweep.enable(),
self.period, self.period,
0, self.counter.current,
self.enabled, self.enabled,
self.period_timer, self.period_timer,
self.cur, // ? self.cur, // ?
@@ -423,6 +419,7 @@ bitfield::bitfield! {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
struct NoiseChannel { struct NoiseChannel {
enabled: bool, enabled: bool,
evelope: NoiseEnvelope, evelope: NoiseEnvelope,
@@ -447,7 +444,7 @@ impl NoiseChannel {
} }
} }
pub fn write(&mut self, offset: u16, val: u8) { pub fn write(&mut self, offset: u16, _val: u8) {
match offset { match offset {
0x00 => (), 0x00 => (),
0x01 => (), 0x01 => (),
@@ -505,7 +502,7 @@ impl DeltaChannel {
} }
} }
pub fn write(&mut self, offset: u16, val: u8) { pub fn write(&mut self, offset: u16, _val: u8) {
match offset { match offset {
0x00 => (), 0x00 => (),
0x01 => (), 0x01 => (),
@@ -553,12 +550,21 @@ pub struct APU {
impl std::fmt::Debug for APU { impl std::fmt::Debug for APU {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!( write!(
// f, f,
// "PPU: f {}, s {}, p {}", "APU
// self.frame_count, self.scanline, self.pixel {:#?}
// ) {:#?}
Ok(()) {:#?}
{:#?}
{:#?}
",
self.pulse_1,
self.pulse_2,
self.triangle,
self.noise,
self.dmc,
)
} }
} }
@@ -577,7 +583,7 @@ impl APU {
irq: false, irq: false,
}, },
samples: vec![0; 100], samples: Vec::with_capacity(10000),
} }
} }
@@ -739,3 +745,6 @@ mod apu_iced {
} }
} }
} }
#[cfg(test)]
mod tests;

98
src/apu/tests.rs Normal file
View File

@@ -0,0 +1,98 @@
use super::*;
#[track_caller]
fn run_ppu_cycles(apu: &mut APU, cycles: usize) {
for i in 0..cycles {
apu.run_one_clock_cycle(i);
}
}
trait Pattern {
fn matches(&mut self, val: u8) -> bool;
}
impl Pattern for u8 {
fn matches(&mut self, val: u8) -> bool {
val == *self
}
}
impl<I: Iterator<Item = u8>> Pattern for I {
fn matches(&mut self, val: u8) -> bool {
self.next().is_some_and(|v| v == val)
}
}
fn is(samples: &[u8], mut pattern: impl Pattern) -> bool {
for s in samples {
if !pattern.matches(*s) {
return false;
}
}
true
}
#[test]
fn check_is() {
assert!(&[0; 20], 0..5);
}
macro_rules! cycle_check {
(|$apu:ident| $init:expr; $([$pat:expr; $count:expr]),* ; [$final:expr; _]) => {
let mut $apu = APU::init();
$init;
run_ppu_cycles(&mut $apu, 10000);
let mut s = $apu.get_frame_samples();
let mut cur = 0;
$(
let count = $count;
if !is(&s[..count], $pat) {
panic!("Incorrect samples from {} to {}:\nExpected: {:?}\n Found: {:?}", cur, cur + count, $pat, &s[..count]);
}
s = &s[count..];
cur += count;
)*
if !is(s, $final) {
panic!("Incorrect samples from {} to {}:\nExpected: {:?}\n Found: {:?}", cur, cur + count, $final, s);
}
};
}
const SNDCHN: u16 = 0x15;
const PULSE_CH1_DLCV: u16 = 0x00;
const PULSE_CH1_SWEEP: u16 = 0x01;
const PULSE_CH1_TLOW: u16 = 0x02;
const PULSE_CH1_LCTH: u16 = 0x03;
#[test]
fn run_apu() {
let mut apu = APU::init();
run_ppu_cycles(&mut apu, 10000);
println!("Count: {}", apu.get_frame_samples().len());
assert_eq!(apu.get_frame_samples(), &[0; 417]);
}
#[test]
fn run_apu_pulse_1() {
// Base? case
cycle_check!(|apu| {
apu.write_reg(SNDCHN, 0x01);
apu.write_reg(PULSE_CH1_DLCV, 0xBF);
apu.write_reg(PULSE_CH1_SWEEP, 0x7F);
apu.write_reg(PULSE_CH1_TLOW, 0x6F);
apu.write_reg(PULSE_CH1_LCTH, 0xF0);
// 1666 APU cycles (every fourth is a sample)
// Advance in duty cycle every 111 + 1 apu cycle (28 samples)
}; [0; 28], [37; 28*4], [0; 28*4], [37; 28*4] ; [0; _]);
cycle_check!(|apu| {
apu.write_reg(SNDCHN, 0x01);
apu.write_reg(PULSE_CH1_DLCV, 0xBF);
apu.write_reg(PULSE_CH1_SWEEP, 0x7F);
apu.write_reg(PULSE_CH1_TLOW, 0x60);
apu.write_reg(PULSE_CH1_LCTH, 0xF0);
// 1666 APU cycles (every fourth is a sample)
// Advance in duty cycle every 96 + 1 apu cycle (24.25 samples)
}; [0; 24], [37; 24*4+1], [0; 24*4+1], [37; 24*4+1], [0; 24*4 + 1] ; [37; _]);
}

View File

@@ -1,12 +1,9 @@
use std::{ use std::sync::{
sync::{ atomic::AtomicBool, Arc
atomic::AtomicBool, mpsc::{channel, Sender, TryRecvError}, Arc };
},
time::Instant,
};
use cpal::{ use cpal::{
Device, FrameCount, Host, SampleFormat, Stream, Device, FrameCount, Host, Stream,
traits::{DeviceTrait, HostTrait, StreamTrait}, traits::{DeviceTrait, HostTrait, StreamTrait},
}; };
use ringbuf::{ use ringbuf::{

View File

@@ -70,13 +70,13 @@ where
{ {
text(format!("{val:02X}")) text(format!("{val:02X}"))
} }
pub fn bin8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer> // pub fn bin8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer>
where // where
Theme: text::Catalog + 'a, // Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer, // Renderer: iced::advanced::text::Renderer,
{ // {
text(format!("{val:08b}")) // text(format!("{val:08b}"))
} // }
pub fn bin32<'a, Theme, Renderer>(val: u32) -> Text<'a, Theme, Renderer> pub fn bin32<'a, Theme, Renderer>(val: u32) -> Text<'a, Theme, Renderer>
where where
Theme: text::Catalog + 'a, Theme: text::Catalog + 'a,
@@ -408,7 +408,7 @@ pub fn labelled_box<'a, Message: 'a>(label: &'a str, value: bool) -> Element<'a,
pub enum DbgImage<'a> { pub enum DbgImage<'a> {
NameTable(&'a Mapped, &'a PPU), NameTable(&'a Mapped, &'a PPU),
PatternTable(&'a Mapped, &'a PPU), PatternTable(&'a Mapped, &'a PPU),
Palette(&'a Mapped, &'a PPU), Palette(&'a PPU),
} }
impl<Message, T> Program<Message, T> for DbgImage<'_> { impl<Message, T> Program<Message, T> for DbgImage<'_> {
@@ -432,7 +432,7 @@ impl<Message, T> Program<Message, T> for DbgImage<'_> {
DbgImage::PatternTable(mem, ppu) => { DbgImage::PatternTable(mem, ppu) => {
ppu.render_pattern_tables(mem, &mut name_table_frame) ppu.render_pattern_tables(mem, &mut name_table_frame)
} }
DbgImage::Palette(_, ppu) => ppu.render_palette(&mut name_table_frame), DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame),
}; };
vec![name_table_frame.into_geometry()] vec![name_table_frame.into_geometry()]
} }
@@ -443,21 +443,21 @@ impl DbgImage<'_> {
match self { match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.), DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2.), DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(40. * 2.), DbgImage::Palette(_) => Length::Fixed(40. * 2.),
} }
} }
fn height(&self) -> Length { fn height(&self) -> Length {
match self { match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.), DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2. * 2.), DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(80. * 2.), DbgImage::Palette(_) => Length::Fixed(80. * 2.),
} }
} }
fn help(&self, cursor: Point) -> Option<String> { fn help(&self, cursor: Point) -> Option<String> {
match self { match self {
DbgImage::NameTable(mem, ppu) => ppu.name_cursor_info(mem, cursor), DbgImage::NameTable(mem, ppu) => ppu.name_cursor_info(mem, cursor),
DbgImage::PatternTable(_, ppu) => ppu.pattern_cursor_info(cursor), DbgImage::PatternTable(_, ppu) => ppu.pattern_cursor_info(cursor),
DbgImage::Palette(_, ppu) => ppu.palette_cursor_info(cursor), DbgImage::Palette(ppu) => ppu.palette_cursor_info(cursor),
} }
} }
} }

View File

@@ -1,25 +1,25 @@
use std::{ use std::fmt::{self, Display};
fmt::{self, Display}, ops::Deref
};
use iced::{ use iced::{
advanced::{layout::{Limits, Node}, overlay, renderer::{Quad, Style}, text::Renderer, widget::{tree::{State, Tag}, Operation, Tree}, Clipboard, Layout, Shell, Text, Widget}, alignment::Vertical, keyboard::{key::Named, Key}, mouse::{Button, Cursor, Interaction, ScrollDelta}, widget::{column, lazy, text}, Color, Element, Event, Fill, Font, Length, Padding, Pixels, Point, Rectangle, Size, Task, Vector Color, Element, Event, Font, Length, Padding, Pixels, Point, Rectangle, Size, Task, Vector,
advanced::{
Clipboard, Layout, Shell, Text, Widget,
layout::{Limits, Node},
overlay,
renderer::{Quad, Style},
text::Renderer,
widget::{
Operation, Tree,
tree::{State, Tag},
},
},
alignment::Vertical,
keyboard::{Key, key::Named},
mouse::{Button, Cursor, Interaction, ScrollDelta},
}; };
// use iced_core::{
// Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector,
// Widget,
// layout::{Limits, Node},
// mouse::{Cursor, Interaction},
// overlay,
// renderer::{Quad, Style},
// widget::{
// Operation, Tree,
// tree::{State, Tag},
// },
// };
use nes_emu::Memory; use nes_emu::Memory;
use nes_emu::{PPU, Mapped, PPUMMRegisters}; use nes_emu::{Mapped, PPU, PPUMMRegisters};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct Cpu<'a>(&'a Mapped); struct Cpu<'a>(&'a Mapped);
@@ -82,53 +82,53 @@ pub enum HexEvent {}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct HexView {} pub struct HexView {}
struct Val(Option<u8>); // struct Val(Option<u8>);
impl fmt::Display for Val { // impl fmt::Display for Val {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(val) = self.0 { // if let Some(val) = self.0 {
write!(f, "{:02X}", val) // write!(f, "{:02X}", val)
} else { // } else {
write!(f, "XX") // write!(f, "XX")
} // }
} // }
} // }
impl HexView { impl HexView {
pub fn new() -> Self { // pub fn new() -> Self {
Self {} // Self {}
} // }
pub fn render_any<'a, M: Memory + Copy + 'a>(&self, mem: M) -> Element<'a, HexEvent> { // pub fn render_any<'a, M: Memory + Copy + 'a>(&self, mem: M) -> Element<'a, HexEvent> {
struct Row<M: Memory>(usize, M); // struct Row<M: Memory>(usize, M);
impl<'a, M: Memory + 'a> fmt::Display for Row<M> { // impl<'a, M: Memory + 'a> fmt::Display for Row<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Val(self.1.peek(self.0)))?; // write!(f, "{}", Val(self.1.peek(self.0)))?;
for i in 1..16 { // for i in 1..16 {
write!(f, " {}", Val(self.1.peek(self.0 + i)))?; // write!(f, " {}", Val(self.1.peek(self.0 + i)))?;
} // }
Ok(()) // Ok(())
} // }
} // }
column![ // column![
text!("Hex view"), // text!("Hex view"),
iced::widget::scrollable(lazy(mem.edit_ver(), move |_| column( // iced::widget::scrollable(lazy(mem.edit_ver(), move |_| column(
[ // [
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F") // text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
.font(Font::MONOSPACE) // .font(Font::MONOSPACE)
.into() // .into()
] // ]
.into_iter() // .into_iter()
.chain((0..mem.len()).step_by(16).map(|off| { // .chain((0..mem.len()).step_by(16).map(|off| {
text!(" {off:04X} | {}", Row(off, mem)) // text!(" {off:04X} | {}", Row(off, mem))
.font(Font::MONOSPACE) // .font(Font::MONOSPACE)
.into() // .into()
})) // }))
))) // )))
.width(Fill), // .width(Fill),
] // ]
.width(Fill) // .width(Fill)
.into() // .into()
} // }
pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> { pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
// self.render_any(Cpu(mem)) // self.render_any(Cpu(mem))
Element::new( Element::new(
@@ -147,35 +147,35 @@ impl HexView {
) )
} }
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> { pub fn update(&mut self, _ev: HexEvent) -> Task<HexEvent> {
todo!() todo!()
} }
} }
pub struct BufferSlice<'a>(pub &'a [u8]); // pub struct BufferSlice<'a>(pub &'a [u8]);
impl Deref for BufferSlice<'_> { // impl Deref for BufferSlice<'_> {
type Target = [u8]; // type Target = [u8];
fn deref(&self) -> &Self::Target { // fn deref(&self) -> &Self::Target {
self.0 // self.0
} // }
} // }
pub trait Buffer { pub trait Buffer {
fn peek(&self, val: usize) -> Option<u8>; fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize; fn len(&self) -> usize;
} }
impl Buffer for BufferSlice<'_> { // impl Buffer for BufferSlice<'_> {
fn peek(&self, val: usize) -> Option<u8> { // fn peek(&self, val: usize) -> Option<u8> {
self.get(val).copied() // self.get(val).copied()
} // }
fn len(&self) -> usize { // fn len(&self) -> usize {
self.iter().len() // self.iter().len()
} // }
} // }
impl<M: Memory> Buffer for M { impl<M: Memory> Buffer for M {
fn peek(&self, val: usize) -> Option<u8> { fn peek(&self, val: usize) -> Option<u8> {
self.peek(val) self.peek(val)
@@ -202,6 +202,7 @@ pub struct HexEdit {
new_value: u8, new_value: u8,
} }
#[allow(dead_code)]
pub struct HexEditor<B, M, R: Renderer> { pub struct HexEditor<B, M, R: Renderer> {
val: B, val: B,
font_size: Option<Pixels>, font_size: Option<Pixels>,
@@ -279,6 +280,7 @@ where
} }
#[derive(Default)] #[derive(Default)]
#[allow(dead_code)]
struct HexEditorState { struct HexEditorState {
offset_y: f32, offset_y: f32,
selected: usize, selected: usize,
@@ -311,10 +313,10 @@ where
&self, &self,
tree: &Tree, tree: &Tree,
renderer: &mut R, renderer: &mut R,
theme: &T, _theme: &T,
style: &Style, _style: &Style,
layout: Layout<'_>, layout: Layout<'_>,
cursor: Cursor, _cursor: Cursor,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let state: &HexEditorState = tree.state.downcast_ref(); let state: &HexEditorState = tree.state.downcast_ref();

View File

@@ -46,11 +46,12 @@ use audio::Audio;
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1_evelope.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1_evelope.nes");
const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_1.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_1.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_triangle.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_triangle.nes");
// const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; // const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = "./cpu_timing_test.nes"; // const ROM_FILE: &str = "./cpu_timing_test.nes";
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes"; // const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
const ROM_FILE: &str = "../Downloads/Legend of Zelda, The (USA).nes";
extern crate nes_emu; extern crate nes_emu;
@@ -453,7 +454,7 @@ impl Emulator {
dbg_image(DbgImage::PatternTable(self.nes.mem(), self.nes.ppu())).into() dbg_image(DbgImage::PatternTable(self.nes.mem(), self.nes.ppu())).into()
} }
Some(WindowType::Palette) => { Some(WindowType::Palette) => {
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into() dbg_image(DbgImage::Palette(self.nes.ppu())).into()
} }
Some(WindowType::Debugger) => column![ Some(WindowType::Debugger) => column![
row![ row![

View File

@@ -1729,7 +1729,12 @@ impl Cpu {
0xEA => inst!("NOP", 1, || { 0xEA => inst!("NOP", 1, || {
log!("{addr:04X}: NOP"); log!("{addr:04X}: NOP");
}), }),
_ => todo!("ins: 0x{:04X}: 0x{ins:X}, {params:X?}", self.pc - 1), // _ => todo!("ins: 0x{:04X}: 0x{ins:X}, {params:X?}", self.pc - 1),
_ => {
println!("ins: 0x{:04X}: 0x{ins:X}, {params:X?}", self.pc - 1);
self.halt();
ExecState::Done
}
} }
} }
@@ -1936,7 +1941,9 @@ impl Cpu {
} }
pub fn reset(&mut self, mem: &mut CpuMem<'_>) { pub fn reset(&mut self, mem: &mut CpuMem<'_>) {
let debug_log = self.debug_log.enabled();
*self = Self::init(); *self = Self::init();
if debug_log { self.debug_log.enable(); }
self.pc = u16::from_le_bytes([mem.read(0xFFFC), mem.read(0xFFFD)]); self.pc = u16::from_le_bytes([mem.read(0xFFFC), mem.read(0xFFFD)]);
self.sp = 0xFD; self.sp = 0xFD;
self.status.set_interrupt_disable(true); self.status.set_interrupt_disable(true);

View File

@@ -1,12 +1,12 @@
use std::num::NonZeroUsize; // use std::num::NonZeroUsize;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DebugLog { pub struct DebugLog {
enabled: bool, enabled: bool,
current: String, current: String,
history: Vec<String>, history: Vec<String>,
max_history: Option<NonZeroUsize>, // max_history: Option<NonZeroUsize>,
pos: usize, // pos: usize,
} }
impl DebugLog { impl DebugLog {
@@ -15,8 +15,8 @@ impl DebugLog {
enabled: false, enabled: false,
current: String::new(), current: String::new(),
history: vec![], history: vec![],
max_history: None, // max_history: None,
pos: 0, // pos: 0,
} }
} }
@@ -53,6 +53,10 @@ impl DebugLog {
pub fn enable(&mut self) { pub fn enable(&mut self) {
self.enabled = true; self.enabled = true;
} }
pub fn enabled(&self) -> bool {
self.enabled
}
} }
impl std::fmt::Write for DebugLog { impl std::fmt::Write for DebugLog {

View File

@@ -9,10 +9,10 @@ mod mem;
mod ppu; mod ppu;
#[cfg(test)] #[cfg(test)]
mod test_roms; mod test_roms;
pub use mem::{Memory, Mapped}; pub use mem::{Mapped, Memory};
pub use ppu::{Color, PPU, RenderBuffer, PPUMMRegisters}; pub use cpu::{CPUMMRegisters, Cpu, CycleResult};
pub use cpu::{Cpu, CycleResult, CPUMMRegisters}; pub use ppu::{Color, PPU, PPUMMRegisters, RenderBuffer};
// #[cfg(not(target_arch = "wasm32"))] // #[cfg(not(target_arch = "wasm32"))]
// use tokio::io::AsyncReadExt as _; // use tokio::io::AsyncReadExt as _;
@@ -260,16 +260,16 @@ impl NES {
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.clock_count = 0; self.clock_count = 0;
// dbg_int: false, // dbg_int: false,
// clock_count: 0, // clock_count: 0,
// mapped, // mapped,
// cpu: Cpu::init(), // cpu: Cpu::init(),
// dma: DmaState::Passive, // dma: DmaState::Passive,
// // ppu: PPU::with_chr_rom(chr_rom, mapper), // // ppu: PPU::with_chr_rom(chr_rom, mapper),
// ppu: PPU::init(), // ppu: PPU::init(),
// apu: APU::init(), // apu: APU::init(),
// controller: Controllers::init(), // controller: Controllers::init(),
self.cpu.reset(&mut CpuMem::new( self.cpu.reset(&mut CpuMem::new(
&mut self.mapped, &mut self.mapped,
&mut self.ppu, &mut self.ppu,
@@ -284,11 +284,11 @@ impl NES {
} }
pub fn power_cycle(&mut self) { pub fn power_cycle(&mut self) {
// self.memory.clear(); let debug_log = self.debug_log().enabled();
// self.ppu.reset();
// self.ppu.memory.clear();
*self = Self::from_mem(self.mapped.power_cylce()); *self = Self::from_mem(self.mapped.power_cylce());
// self.memory.power_cycle(); if debug_log {
self.debug_log_mut().enable();
}
self.reset(); self.reset();
} }

View File

@@ -1,8 +1,9 @@
use std::{fmt, sync::Arc}; use std::{fmt, sync::Arc};
use bitfield::bitfield;
use crate::{ use crate::{
CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState, CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState, ppu::PPUMMRegisters,
ppu::PPUMMRegisters,
}; };
pub trait Memory { pub trait Memory {
@@ -32,7 +33,7 @@ pub enum Data<R> {
ROM(Arc<[u8]>), ROM(Arc<[u8]>),
Mirror(u16), Mirror(u16),
Reg(R), Reg(R),
Disabled, // Disabled,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -139,8 +140,7 @@ impl<R: Copy> MemoryMap<R> {
self.read(pos + offset) self.read(pos + offset)
// let s = &self.segments[*index]; // let s = &self.segments[*index];
// self.read(s.position + offset % s.size) // self.read(s.position + offset % s.size)
} } // Data::Disabled => todo!(),
Data::Disabled => todo!(),
}; };
} }
} }
@@ -167,8 +167,7 @@ impl<R: Copy> MemoryMap<R> {
// let index = *index; // let index = *index;
// let s = &self.segments[index]; // let s = &self.segments[index];
// self.write(s.position + offset % s.size, val) // self.write(s.position + offset % s.size, val)
} } // Data::Disabled => todo!(),
Data::Disabled => todo!(),
}; };
} }
} }
@@ -177,14 +176,14 @@ impl<R: Copy> MemoryMap<R> {
// todo!("Open bus (${addr:04X})") // todo!("Open bus (${addr:04X})")
} }
pub fn clear(&mut self) { // pub fn clear(&mut self) {
for s in &mut self.segments { // for s in &mut self.segments {
match &mut s.mem { // match &mut s.mem {
Data::RAM(items) => items.fill(0), // Data::RAM(items) => items.fill(0),
_ => (), // _ => (),
} // }
} // }
} // }
pub fn power_cycle(&mut self) -> Self { pub fn power_cycle(&mut self) -> Self {
for seg in &mut self.segments { for seg in &mut self.segments {
@@ -217,8 +216,7 @@ impl<R: Copy> MemoryMap<R> {
Data::Mirror(pos) => { Data::Mirror(pos) => {
let offset = addr - segment.position; let offset = addr - segment.position;
self.peek_val(pos + offset) self.peek_val(pos + offset)
} } // Data::Disabled => Err(None),
Data::Disabled => Err(None),
}; };
} }
} }
@@ -244,13 +242,38 @@ impl<R: Copy> Memory for MemoryMap<R> {
} }
} }
bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
struct ShiftPair(u8);
impl Debug;
high, set_high: 7, 4;
low, set_low: 3, 0;
}
impl ShiftPair {
fn low_count(&self) -> usize {
if self.low() == 0 { 0 } else { 64 << self.low() }
}
fn high_count(&self) -> usize {
if self.high() == 0 {
0
} else {
64 << self.high()
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
struct Mapper { struct Mapper {
is_nes_2: bool,
horizontal_name_table: bool, horizontal_name_table: bool,
mapper: u16, mapper: u16,
sub_mapper: u8, sub_mapper: u8,
prg_rom_size: u8, prg_rom_size: u8,
chr_rom_size: u8, chr_rom_size: u8,
prg_nv_ram_size: ShiftPair,
chr_nv_ram_size: ShiftPair,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -279,6 +302,8 @@ enum SegmentId {
PrgBank1, PrgBank1,
ChrBank0, ChrBank0,
ChrBank1, ChrBank1,
NVRam,
} }
impl fmt::Display for SegmentId { impl fmt::Display for SegmentId {
@@ -304,10 +329,11 @@ impl Mapped {
// } // }
let mapper = if nes_20 { let mapper = if nes_20 {
assert_eq!(header[9], 0, "No support for larger PRG/CHR roms"); assert_eq!(header[9], 0, "No support for larger PRG/CHR roms");
assert_eq!(header[10], 0, "No support for PRG-RAM/EEPROM"); // assert_eq!(header[10], 0, "No support for PRG-RAM/EEPROM");
assert_eq!(header[11], 0, "No support for CHR-RAM"); // assert_eq!(header[11], 0, "No support for CHR-RAM");
assert!(header[12] == 0 || header[12] == 2, "Only support NTSC NES"); assert!(header[12] == 0 || header[12] == 2, "Only support NTSC NES");
Mapper { Mapper {
is_nes_2: true,
horizontal_name_table: header[6] & (1 << 0) == 1, horizontal_name_table: header[6] & (1 << 0) == 1,
mapper: ((header[6] as u16 & 0xF0) >> 4) mapper: ((header[6] as u16 & 0xF0) >> 4)
| ((header[7] as u16 & 0xF0) >> 0) | ((header[7] as u16 & 0xF0) >> 0)
@@ -315,18 +341,39 @@ impl Mapped {
sub_mapper: (header[8] & 0xF0) >> 4, sub_mapper: (header[8] & 0xF0) >> 4,
prg_rom_size: header[4], prg_rom_size: header[4],
chr_rom_size: header[5], chr_rom_size: header[5],
prg_nv_ram_size: ShiftPair(header[10]),
chr_nv_ram_size: ShiftPair(header[11]),
} }
} else { } else {
Mapper { Mapper {
is_nes_2: false,
horizontal_name_table: header[6] & (1 << 0) == 1, horizontal_name_table: header[6] & (1 << 0) == 1,
mapper: ((header[6] as u16 & 0xF0) >> 4) | ((header[7] as u16 & 0xF0) >> 0), mapper: ((header[6] as u16 & 0xF0) >> 4) | ((header[7] as u16 & 0xF0) >> 0),
sub_mapper: 0, sub_mapper: 0,
prg_rom_size: header[4], prg_rom_size: header[4],
chr_rom_size: header[5], chr_rom_size: header[5],
prg_nv_ram_size: ShiftPair(0),
chr_nv_ram_size: ShiftPair(0),
} }
}; };
let (prg_rom, rom) = rom.split_at(mapper.prg_rom_size as usize * 0x4000); let (prg_rom, rom) = rom.split_at(mapper.prg_rom_size as usize * 0x4000);
let (chr_rom, rom) = rom.split_at(mapper.chr_rom_size as usize * 0x2000); let (chr_rom, _rom) = rom.split_at(mapper.chr_rom_size as usize * 0x2000);
println!("Mapper: {}/{}", mapper.mapper, mapper.sub_mapper);
assert_eq!(
mapper.prg_nv_ram_size.low_count(),
0,
"No support for PRG-RAM"
);
// assert_eq!(
// mapper.chr_nv_ram_size.low_count(),
// 0,
// "No support for CHR-RAM"
// );
assert_eq!(
mapper.chr_nv_ram_size.high_count(),
0,
"No support for CHR-NVRAM"
);
// assert_eq!(rom.len(), 0); // assert_eq!(rom.len(), 0);
// let prg_rom = &file[self.cpu_offset()..self.ppu_offset()]; // let prg_rom = &file[self.cpu_offset()..self.ppu_offset()];
let mut cpu_segments = vec![ let mut cpu_segments = vec![
@@ -340,6 +387,14 @@ impl Mapped {
CPUMMRegisters::APU, CPUMMRegisters::APU,
), ),
]; ];
// CHR-NVRAM
if mapper.prg_nv_ram_size.high_count() > 0 {
cpu_segments.push(Segment::ram(
SegmentId::NVRam,
0x6000,
mapper.prg_nv_ram_size.high_count() as u16,
));
}
let mut ppu_segments = vec![ let mut ppu_segments = vec![
Segment::mirror(SegmentId::VramMirror, 0x3000, 0x0F00, 0x0000), Segment::mirror(SegmentId::VramMirror, 0x3000, 0x0F00, 0x0000),
Segment::reg( Segment::reg(
@@ -387,10 +442,22 @@ impl Mapped {
0x8000 + (0x8000 - prg_rom.len() as u16), 0x8000 + (0x8000 - prg_rom.len() as u16),
prg_rom, prg_rom,
)); ));
if chr_rom.len() == 0 { if mapper.is_nes_2 {
ppu_segments.push(Segment::ram(SegmentId::PpuRam, 0, 0x2000)); if mapper.chr_nv_ram_size.low_count() > 0 {
ppu_segments.push(Segment::ram(
SegmentId::PpuRam,
0,
mapper.chr_nv_ram_size.low_count() as u16,
));
} else {
ppu_segments.push(Segment::rom(SegmentId::PpuRom, 0, chr_rom));
}
} else { } else {
ppu_segments.push(Segment::rom(SegmentId::PpuRom, 0, chr_rom)); if chr_rom.len() == 0 {
ppu_segments.push(Segment::ram(SegmentId::PpuRam, 0, 0x2000));
} else {
ppu_segments.push(Segment::rom(SegmentId::PpuRom, 0, chr_rom));
}
} }
Remapper::None Remapper::None
} else if mapper.mapper == 1 && mapper.sub_mapper == 0 { } else if mapper.mapper == 1 && mapper.sub_mapper == 0 {
@@ -550,7 +617,7 @@ impl<'a> CpuMem<'a> {
*shift_reg = 0; *shift_reg = 0;
*count = 0; *count = 0;
} else if *count == 4 { } else if *count == 4 {
let val = (*shift_reg << 1) | (val & 0x01); let val = (*shift_reg >> 1) | ((val & 0x01) << 4);
if (addr & 0x6000) >> 13 == 0 { if (addr & 0x6000) >> 13 == 0 {
// TODO: fix mem layout if it's changed // TODO: fix mem layout if it's changed
if val & 0b01100 == 0b01000 { if val & 0b01100 == 0b01000 {
@@ -591,6 +658,7 @@ impl<'a> CpuMem<'a> {
} }
} else if (addr & 0x6000) >> 13 == 3 { } else if (addr & 0x6000) >> 13 == 3 {
*cur_prg_bank = (val & 0x0F) as usize; *cur_prg_bank = (val & 0x0F) as usize;
println!("Updating ROM: new {:X}", val);
match mode { match mode {
MMC1Mode::Full => { MMC1Mode::Full => {
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap(); let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
@@ -600,11 +668,13 @@ impl<'a> CpuMem<'a> {
} }
MMC1Mode::FirstFixed => { MMC1Mode::FirstFixed => {
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap(); let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks[(val & 0x0F) as usize].clone()); // Highest bit is ignored
bank1.swap_rom(prg_banks[(val & 0x07) as usize].clone());
} }
MMC1Mode::LastFixed => { MMC1Mode::LastFixed => {
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap(); let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[(val & 0x0F) as usize].clone()); // Highest bit is ignored
bank0.swap_rom(prg_banks[(val & 0x07) as usize].clone());
} }
} }
// TODO: handle MSB, changes some stuff... // TODO: handle MSB, changes some stuff...
@@ -614,8 +684,9 @@ impl<'a> CpuMem<'a> {
*shift_reg = 0; *shift_reg = 0;
*count = 0; *count = 0;
} else { } else {
*shift_reg = (*shift_reg << 1) | (val & 0x01); *shift_reg = (*shift_reg >> 1) | ((val & 0x01) << 4);
*count += 1; *count += 1;
println!("Mapper SHR {:05b}", *shift_reg);
} }
} }
_ => (), _ => (),

View File

@@ -107,7 +107,7 @@ enum OamState {
ReadTile, ReadTile,
ReadAttrs, ReadAttrs,
ReadX, ReadX,
OverflowScan, // OverflowScan,
Wait, Wait,
} }
@@ -194,7 +194,7 @@ impl OAM {
self.state = OamState::Wait; // Should be Overflow scan... self.state = OamState::Wait; // Should be Overflow scan...
} }
} }
OamState::OverflowScan => todo!(), // OamState::OverflowScan => todo!(),
OamState::Wait => (), OamState::Wait => (),
} }
} }

View File

@@ -0,0 +1,33 @@
.include "testing.s"
zp_res TIMER_LOW
zp_res SWEEP
zp_res DLCV
; zp_res TIMER_LOW
reset:
sei
cld
ldx #$FF
txs
lda #$01
sta SNDCHN
lda #$BF ; Duty 2, LC halted, evelope enabled, volume = F
sta PULSE_CH1_DLCV
sta DLCV
lda #$7F ;
sta PULSE_CH1_SWEEP
sta SWEEP
lda #$6F
sta PULSE_CH1_TLOW
sta TIMER_LOW
lda #$F0
sta PULSE_CH1_LCTH
brk
loop:
jmp loop
irq:
nmi:
rti

View File

@@ -10,28 +10,28 @@ macro_rules! rom_test {
($name:ident, $rom:literal, timeout = $timeout:expr, |$nes:ident| $eval:expr) => { ($name:ident, $rom:literal, timeout = $timeout:expr, |$nes:ident| $eval:expr) => {
#[test] #[test]
fn $name() { fn $name() {
let rom_file = concat!(env!("ROM_DIR"), "/", $rom); let rom_file = include_bytes!(concat!(env!("ROM_DIR"), "/", $rom));
println!("{}: {}", stringify!($name), rom_file); println!("{}: {}", stringify!($name), concat!(env!("ROM_DIR"), "/", $rom));
let mut $nes = crate::NES::load_nes_file(rom_file).expect("Failed to create nes object"); let mut $nes = crate::NES::load_nes_file_mem(rom_file).expect("Failed to create nes object");
$nes.reset_and_run_with_timeout($timeout);
println!("Final: {:?}", $nes);
$eval
}
};
($name:ident, . $rom:literal, |$nes:ident| $eval:expr) => {
rom_test!($name, . $rom, timeout = 10000000, |$nes| $eval);
};
($name:ident, . $rom:literal, timeout = $timeout:expr, |$nes:ident| $eval:expr) => {
#[test]
fn $name() {
let rom_file = $rom;
println!("{}: {}", stringify!($name), rom_file);
let mut $nes = crate::NES::load_nes_file(rom_file).expect("Failed to create nes object");
$nes.reset_and_run_with_timeout($timeout); $nes.reset_and_run_with_timeout($timeout);
println!("Final: {:?}", $nes); println!("Final: {:?}", $nes);
$eval $eval
} }
}; };
// ($name:ident, . $rom:literal, |$nes:ident| $eval:expr) => {
// rom_test!($name, . $rom, timeout = 10000000, |$nes| $eval);
// };
// ($name:ident, . $rom:literal, timeout = $timeout:expr, |$nes:ident| $eval:expr) => {
// #[test]
// fn $name() {
// let rom_file = $rom;
// println!("{}: {}", stringify!($name), rom_file);
// let mut $nes = crate::NES::load_nes_file(rom_file).expect("Failed to create nes object");
// $nes.reset_and_run_with_timeout($timeout);
// println!("Final: {:?}", $nes);
// $eval
// }
// };
} }
pub(crate) use rom_test; pub(crate) use rom_test;